import json
import re
from difflib import SequenceMatcher
from typing import Dict, Any, Optional, List, Union
from fastapi import HTTPException
from config import Config
from models import UnitDateParams, SettFileDownParams
from base_service import BaseService
import aiohttp
from llama_index.llms.openai_like import OpenAILike
from llama_index.core.base.llms.types import ChatMessage
import io
import base64
import matplotlib

# 导入模糊关键词处理功能
from utils_2 import extract_settlement_keyword_with_ambiguity_check
matplotlib.use('Agg')
# 导入字体管理模块
from matplotlib.font_manager import fontManager, FontProperties
import platform
import os

# 添加项目字体目录
# 尝试多个可能的字体目录
possible_font_dirs = [
    # 基于当前文件位置的相对路径
    os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fonts'),
    # 基于当前工作目录的相对路径
    os.path.join(os.getcwd(), 'fonts'),
    # Linux环境下的可能路径
    '/home/jiuyv/ccagent/fonts',
    '/home/jiuyy/ccagent/fonts',
]

project_fonts_dir = None
for font_dir in possible_font_dirs:
    if os.path.exists(font_dir):
        project_fonts_dir = font_dir
        print(f"找到字体目录: {font_dir}")
        break

if project_fonts_dir:
    # 遍历项目字体目录中的所有字体文件
    for font_file in os.listdir(project_fonts_dir):
        if font_file.endswith(('.ttf', '.ttc', '.otf')):
            font_path = os.path.join(project_fonts_dir, font_file)
            try:
                fontManager.addfont(font_path)
                print(f"已加载项目字体: {font_file}")
            except Exception as e:
                print(f"加载字体失败 {font_file}: {str(e)}")
else:
    print("未找到可用的字体目录")

# 根据操作系统设置字体列表
if platform.system() == 'Windows':
    # Windows系统使用自带中文字体
    font_list = ["Microsoft YaHei", "SimHei", "SimSun", "KaiTi", "FangSong", "YouYuan", "LiSu", "STHeiti", "STSong", "STFangsong", "STKaiti", "sans-serif"]
elif platform.system() == 'Linux':
    # Linux系统使用常用中文字体
    font_list = ["WenQuanYi Micro Hei", "WenQuanYi Zen Hei", "Noto Sans CJK SC", "SimHei", "Microsoft YaHei", "sans-serif"]
else:
    # 其他系统（如macOS）
    font_list = ["PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "SimHei", "sans-serif"]

# 设置matplotlib字体
matplotlib.rcParams["font.family"] = font_list
matplotlib.rcParams['axes.unicode_minus'] = False
import matplotlib.pyplot as plt
import logging
import os
from datetime import datetime
from chat_history import chat_history_manager
import time
from cachetools import TTLCache
import asyncio
from concurrent.futures import ThreadPoolExecutor, TimeoutError
import hashlib
import aiofiles
from user_query_logger import log_user_query

# 初始化基础服务
service = BaseService()

# LLM调用缓存，最大1000个条目，1小时过期
llm_cache = TTLCache(maxsize=1000, ttl=3600)

class LLMService:
    @staticmethod
    def get_llm(temperature: float = 0.1, max_tokens: int = 2048):
        return OpenAILike(
            api_key=Config.QWEN_API_KEY,
            api_base=Config.QWEN_API_BASE.rstrip('/'),
            model=Config.QWEN_MODEL,
            is_chat_model=True,
            temperature=temperature,
            max_tokens=max_tokens
        )

    @staticmethod
    def clear_cache():
        """清除LLM缓存"""
        global llm_cache
        llm_cache.clear()
        logging.info("[LLM缓存] 已清除所有缓存")

    @staticmethod
    def _generate_cache_key(prompt: str, temperature: float, max_tokens: int) -> str:
        """生成缓存键"""
        cache_string = f"{prompt}:{temperature}:{max_tokens}"
        return hashlib.md5(cache_string.encode()).hexdigest()

    @staticmethod
    async def call_qwen_model(prompt: str, temperature: float = 0.0, max_tokens: int = 1024, use_cache: bool = True, **kwargs) -> str:
        start_time = time.time()
        timeout = kwargs.get('timeout', 60)  # 默认60秒超时
        
        # 检查是否为监控请求，监控请求会包含特殊标识
        is_monitor = "[MONITOR_" in prompt
        if is_monitor:
            # 监控请求强制绕过缓存
            use_cache = False
            logging.info(f"[步骤3-大模型调用] 检测到监控请求，绕过缓存机制")
        
        # 生成缓存键
        cache_key = LLMService._generate_cache_key(prompt, temperature, max_tokens) if use_cache else None
        
        # 尝试从缓存获取
        if cache_key and cache_key in llm_cache:
            logging.info(f"[步骤3-大模型调用] LLM缓存命中")
            return llm_cache[cache_key]
        
        try:
            # 在事件循环中运行同步LLM调用
            loop = asyncio.get_event_loop()
            llm = LLMService.get_llm(temperature=temperature, max_tokens=max_tokens)
            
            def _sync_call():
                response = llm.chat([ChatMessage(role="user", content=prompt)])
                content = response.message.content if hasattr(response, 'message') and response.message and hasattr(response.message, 'content') else str(response)
                return content.strip() if content else ""
            
            # 使用run_in_executor在单独的线程中运行同步调用，并添加超时控制
            result = await asyncio.wait_for(loop.run_in_executor(None, _sync_call), timeout=timeout)
            
            # 缓存结果
            if cache_key:
                llm_cache[cache_key] = result
                logging.debug(f"[LLM缓存] 存储结果: {cache_key}")
            
            end_time = time.time()
            logging.info(f"[步骤3-大模型调用] 大模型调用耗时: {end_time - start_time:.2f}秒")
            return result
        except asyncio.TimeoutError:
            end_time = time.time()
            logging.error(f"[步骤3-大模型调用] 大模型调用超时: {timeout}秒, 耗时: {end_time - start_time:.2f}秒")
            raise HTTPException(status_code=504, detail=f"大模型调用超时，请稍后重试")
        except Exception as e:
            end_time = time.time()
            logging.error(f"[步骤3-大模型调用] 大模型调用出错: {str(e)}, 耗时: {end_time - start_time:.2f}秒")
            raise

# 参数转换工具
async def convert_params(user_query: str, param_type: str, phone_number: str, history: Optional[List[Dict[str, Any]]] = None, is_user_selection: bool = False, selected_unit_info: Dict = None) -> dict:
    from datetime import datetime, timedelta
    start_time = time.time()
    logging.info(f"[步骤1-参数转换] 开始参数转换")
    
    # 如果是用户数字选择场景，直接使用已选择的单位信息，避免重复匹配
    if is_user_selection and selected_unit_info:
        logging.info(f"[参数转换] 用户数字选择场景，使用已选择的单位信息: {selected_unit_info}")
        # 直接构建参数，不进行单位匹配
        param_templates = {
            "invoice": f"请将用户的自然语言需求转换为发票查询接口的JSON参数:\n必须参数：startDate(开始日期YYYYMMDD)、endDate(结束日期YYYYMMDD)\n只返回JSON对象，不要其他文字。（今天是{datetime.now().strftime('%Y年%m月%d日')}）。",
            "sett": f"请将用户的自然语言需求转换为轧差数据查询接口的JSON参数:\n必须参数：startDate(开始日期YYYYMMDD)、endDate(结束日期YYYYMMDD)\n只返回JSON对象，不要其他文字。（今天是{datetime.now().strftime('%Y年%m月%d日')}）。",
            "settDown": f"请将用户的自然语言需求转换为轧差表下载接口的JSON参数:\n必须参数：fileId(文件ID)、tbNo(轧差表编号)\n只返回JSON对象，不要其他文字。（今天是{datetime.now().strftime('%Y年%m月%d日')}）。"
        }
        
        if param_type not in param_templates:
            raise HTTPException(status_code=400, detail=f"不支持的参数类型: {param_type}")
        
        prompt = f"{param_templates[param_type]}\n用户需求：{user_query}"
        
        param_json = await LLMService.call_qwen_model(prompt, timeout=30)
        logging.info(f"[LLM调用] 参数转换结果: {param_json[:100]}...")
        
        cleaned_json = clean_json_response(param_json)
        
        try:
            params = json.loads(cleaned_json)
            # 验证必要参数
            if param_type in ["invoice", "sett"]:
                if "startDate" not in params or "endDate" not in params:
                    raise ValueError(f"缺少必要的时间参数: {cleaned_json}")
            
            # 使用已选择的单位信息
            params["unitId"] = selected_unit_info.get("unitId")
            params["phoneNo"] = phone_number
            return params
        except json.JSONDecodeError as e:
            logging.error(f"[参数转换] JSON解析失败: {str(e)}, 原始内容: {param_json}")
            raise HTTPException(status_code=500, detail=f"模型返回非JSON格式：{param_json}")
        except ValueError as e:
            logging.error(f"[参数转换] 参数验证失败: {str(e)}")
            raise HTTPException(status_code=400, detail=str(e))
    
    # 处理ETC结算数据查询参数转换
    if param_type == "etc_sett":
        logging.info(f"[参数转换] 处理ETC结算数据查询参数转换")
        
        # 获取用户关联的商户列表
        merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
        
        # 使用LLM转换用户查询为API参数
        prompt = f"请将用户的自然语言需求转换为ETC结算数据查询接口的JSON参数:\n"
        prompt += f"必须参数：startDate(开始日期YYYYMMDD)、endDate(结束日期YYYYMMDD)\n"
        prompt += f"可选参数：subStationIds(商户ID列表，如不提供则查询所有商户)\n"
        
        # 如果获取到商户列表，添加到提示词中
        if merchants:
            # 限制商户列表显示，避免提示词过长
            if len(merchants) > 10:
                # 如果商户数量超过10个，只显示前10个
                display_merchants = merchants[:10]
                prompt += f"可用商户列表（显示前10个，共{len(merchants)}个）：{json.dumps([{'merId': m['merId'], 'merName': m['merName']} for m in display_merchants], ensure_ascii=False)}\n"
            else:
                prompt += f"可用商户列表：{json.dumps([{'merId': m['merId'], 'merName': m['merName']} for m in merchants], ensure_ascii=False)}\n"
        else:
            # 未获取到商户列表，ETC结算需要商户ID，无法处理
            logging.warning(f"[ETC结算] 未获取到商户列表，ETC结算需要商户ID，无法处理")
            prompt += "注意：未获取到商户列表，ETC结算查询需要商户ID，请确保用户有可查询的商户。\n"
            
        # 使用动态当前日期
        current_date = datetime.now().strftime("%Y年%m月%d日")
        prompt += f"只返回JSON对象，不要其他文字。（今天是{current_date}）。\n"
        prompt += f"用户需求：{user_query}"
        
        # 如果有对话历史，添加到prompt中
        if history:
            # 增加历史记录数量，取最近5条历史对话，每条限制300字符，提供更充分的上下文
            history_str = "\n".join([f"{msg['role']}: {msg['content'][:300]}" for msg in history[-5:]])
            prompt = f"对话历史：\n{history_str}\n\n请参考上述对话历史，理解用户需求的上下文，并{prompt}"
        
        param_json = await LLMService.call_qwen_model(prompt, timeout=30)
        logging.info(f"[LLM调用] ETC结算参数转换结果: {param_json[:100]}...")
        
        # 清理JSON字符串
        cleaned_json = clean_json_response(param_json)
        
        try:
            params = json.loads(cleaned_json)
            
            # 验证必要参数，如果缺少时间参数，使用默认值（当月）
            current_date = datetime.now()
            current_year = current_date.year
            current_month = current_date.month
            
            # 如果没有提供startDate，默认为当月第一天
            if "startDate" not in params or not params["startDate"]:
                params["startDate"] = f"{current_year}{current_month:02d}01"
                logging.info(f"[ETC结算] 未提供startDate，使用默认值: {params['startDate']}")
            
            # 如果没有提供endDate，默认为当月最后一天
            if "endDate" not in params or not params["endDate"]:
                # 计算当月最后一天
                import calendar
                last_day = calendar.monthrange(current_year, current_month)[1]
                params["endDate"] = f"{current_year}{current_month:02d}{last_day:02d}"
                logging.info(f"[ETC结算] 未提供endDate，使用默认值: {params['endDate']}")
            
            # 处理subStationIds参数
            if "subStationIds" in params:
                # 确保subStationIds是列表
                if not isinstance(params['subStationIds'], list):
                    params['subStationIds'] = [params['subStationIds']]
                
                # 检查subStationIds是否为空列表
                if len(params['subStationIds']) == 0:
                    # 如果有商户列表，要求用户必须指定
                    if merchants:
                        raise ValueError("请指定要查询的结算站点，不能查询全量数据")
                    # 如果没有商户列表，允许空列表，让系统尝试查询所有数据
                
                # 如果有商户列表，验证商户ID是否在可用列表中
                if merchants:
                    valid_mer_ids = [m['merId'] for m in merchants]
                    for mer_id in params['subStationIds']:
                        if mer_id not in valid_mer_ids:
                            raise ValueError(f"无效的商户ID: {mer_id}")
            else:
                # 如果未提供subStationIds
                if merchants:
                    # 如果有商户列表，要求用户必须指定
                    raise ValueError("请指定要查询的结算站点，不能查询全量数据")
                # 如果没有商户列表，ETC结算需要商户ID，无法处理
                raise ValueError("未获取到商户列表，ETC结算查询需要商户ID，请确保用户有可查询的商户")
            
            # 添加用户ID
            params['userId'] = phone_number
            
            end_time = time.time()
            logging.info(f"[步骤1-参数转换] ETC结算参数转换耗时: {end_time - start_time:.2f}秒")
            return params
        except json.JSONDecodeError as e:
            logging.error(f"[ETC结算参数转换] JSON解析失败: {str(e)}, 原始内容: {param_json}")
            raise HTTPException(status_code=500, detail=f"模型返回非JSON格式：{param_json}")
        except ValueError as e:
            logging.error(f"[ETC结算参数转换] 参数验证失败: {str(e)}")
            raise HTTPException(status_code=400, detail=str(e))
    
    # 处理ETC发票参数转换
    if param_type == "etc_invoice":
        logging.info(f"[参数转换] 处理ETC发票参数转换")
        
        # 首先获取用户关联的商户列表
        merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
        
        # 使用LLM转换用户查询为API参数
        prompt = f"请将用户的自然语言需求转换为ETC发票查询接口的JSON参数:\n"
        prompt += f"必须参数：kpMerid(开票商户ID列表)、kpMonth(开票月份，格式YYYYMM)、start(开始页数，从0开始)、limit(每页条数)\n"
        prompt += f"可选参数：kpStat(开票状态，如\"01\"=成功)、kpType(开票类型，如\"10\"=电子票)\n"
        
        # 处理特殊时间描述：上半年和下半年
        current_year = datetime.now().year
        if "上半年" in user_query:
            # 上半年指1-6月，这里我们使用6月作为代表
            prompt += f"注意：用户查询中的'上半年'指{current_year}年1-6月，请使用{current_year}06作为kpMonth参数值。\n"
        elif "下半年" in user_query:
            # 下半年指7-12月，这里我们使用12月作为代表
            prompt += f"注意：用户查询中的'下半年'指{current_year}年7-12月，请使用{current_year}12作为kpMonth参数值。\n"
        
        # 如果获取到商户列表，添加到提示词中
        if merchants:
            # 限制商户列表显示，避免提示词过长
            if len(merchants) > 10:
                # 如果商户数量超过10个，只显示前10个
                display_merchants = merchants[:10]
                prompt += f"可用商户列表（显示前10个，共{len(merchants)}个）：{json.dumps([{'merId': m['merId'], 'merName': m['merName']} for m in display_merchants], ensure_ascii=False)}\n"
            else:
                prompt += f"可用商户列表：{json.dumps([{'merId': m['merId'], 'merName': m['merName']} for m in merchants], ensure_ascii=False)}\n"
        else:
            # 如果没有获取到商户列表，不再获取清算单位列表
            # 清算单位应该走普通路由，而不是ETC路由
            logging.warning(f"[ETC发票] 未获取到商户列表，ETC发票需要商户ID，无法处理")
            prompt += "注意：未获取到商户列表，ETC发票查询需要商户ID，请确保用户有可查询的商户。\n"
            
        # 使用动态当前日期
        current_date = datetime.now().strftime("%Y年%m月%d日")
        prompt += f"只返回JSON对象，不要其他文字。（今天是{current_date}）。\n"
        prompt += f"用户需求：{user_query}"
        
        # 如果有对话历史，添加到prompt中
        if history:
            # 增加历史记录数量，取最近5条历史对话，每条限制300字符，提供更充分的上下文
            history_str = "\n".join([f"{msg['role']}: {msg['content'][:300]}" for msg in history[-5:]])
            prompt = f"对话历史：\n{history_str}\n\n请参考上述对话历史，理解用户需求的上下文，并{prompt}"
        
        param_json = await LLMService.call_qwen_model(prompt, timeout=30)
        logging.info(f"[LLM调用] ETC参数转换结果: {param_json[:100]}...")
        
        # 清理JSON字符串
        cleaned_json = clean_json_response(param_json)
        
        try:
            params = json.loads(cleaned_json)
            
            # 验证必要参数，如果缺少时间参数，使用默认值（当月）
            current_date = datetime.now()
            current_year = current_date.year
            current_month = current_date.month
            
            # 如果没有提供kpMonth，默认为当前月份
            if "kpMonth" not in params or not params["kpMonth"]:
                params["kpMonth"] = f"{current_year}{current_month:02d}"
                logging.info(f"[ETC发票] 未提供kpMonth，使用默认值: {params['kpMonth']}")
            
            # 验证其他必要参数
            if "kpMerid" not in params or "start" not in params or "limit" not in params:
                raise ValueError(f"缺少必要参数: {cleaned_json}")
            
            # 确保kpMerid是列表
            if not isinstance(params['kpMerid'], list):
                params['kpMerid'] = [params['kpMerid']]
            
            # 检查kpMerid是否为空列表
            if len(params['kpMerid']) == 0:
                # 如果有商户列表，要求用户必须指定
                if merchants:
                    raise ValueError("请指定要查询的开票商户，不能查询全量数据")
                # 如果没有商户列表，ETC发票需要商户ID，无法处理
                raise ValueError("未获取到商户列表，ETC发票查询需要商户ID，请确保用户有可查询的商户")
            
            # 如果有商户列表，验证商户ID是否在可用列表中
            if merchants:
                valid_mer_ids = [m['merId'] for m in merchants]
                for mer_id in params['kpMerid']:
                    if mer_id not in valid_mer_ids:
                        raise ValueError(f"无效的商户ID: {mer_id}")
            
            # 添加用户ID
            params['userId'] = phone_number
            
            end_time = time.time()
            logging.info(f"[步骤1-参数转换] ETC参数转换耗时: {end_time - start_time:.2f}秒")
            return params
        except json.JSONDecodeError as e:
            logging.error(f"[ETC参数转换] JSON解析失败: {str(e)}, 原始内容: {param_json}")
            raise HTTPException(status_code=500, detail=f"模型返回非JSON格式：{param_json}")
        except ValueError as e:
            logging.error(f"[ETC参数转换] 参数验证失败: {str(e)}")
            raise HTTPException(status_code=400, detail=str(e))
    
    # 处理其他类型的参数转换
    # 首先检查是否可以从意图识别阶段获取匹配的单位信息
    unit_ids = None
    
    # 尝试从意图识别阶段获取匹配的单位信息
    try:
        # 重新调用意图识别函数，获取匹配的单位信息
        from .utils import detect_intent_by_user_resources
        
        # 创建一个简化的意图识别函数来获取匹配的单位
        async def get_matched_units_from_intent(user_query: str, phone_number: str, history: Optional[List[Dict[str, Any]]] = None):
            """从意图识别阶段获取匹配的单位信息"""
            try:
                # 获取用户绑定的清算单位列表
                available_units = await get_user_available_units(phone_number, return_full_info=True)
                
                # 提取用户查询中的关键词
                unit_prompt = f"""从用户查询中提取可能匹配清算单位名称的关键词。
用户查询：{user_query}

只返回提取的关键词列表，每行一个，不要添加其他文字。如果没有找到关键词，返回"无"。"""
                
                unit_keywords = []
                try:
                    unit_result = await LLMService.call_qwen_model(unit_prompt, timeout=10)
                    if unit_result.strip() and unit_result.strip() != "无":
                        unit_keywords = [kw.strip() for kw in unit_result.strip().split('\n') if kw.strip()]
                except Exception:
                    # 如果LLM提取失败，使用简单的关键词匹配
                    import re
                    # 提取可能的中文单位名称关键词
                    chinese_pattern = r'[\u4e00-\u9fff]+(?:公司|单位|企业|出租|石油|燃气|公交|集团)'
                    unit_keywords = re.findall(chinese_pattern, user_query)
                
                # 匹配单位关键词
                matched_units = []
                if unit_keywords and available_units:
                    for unit in available_units:
                        unit_name = safe_extract_unit_name(unit)
                        for keyword in unit_keywords:
                            if keyword.lower() in unit_name.lower() or unit_name.lower() in keyword.lower():
                                matched_units.append(unit.get("unitId"))
                                logging.info(f"[意图识别单位匹配] 匹配到单位: {unit_name} (ID: {unit.get('unitId')})")
                                break
                
                return matched_units
            except Exception as e:
                logging.warning(f"[意图识别单位匹配] 获取匹配单位失败: {str(e)}")
                return []
        
        # 获取意图识别阶段匹配的单位
        intent_matched_units = await get_matched_units_from_intent(user_query, phone_number, history)
        
        if intent_matched_units:
            unit_ids = intent_matched_units
            logging.info(f"[参数转换] 从意图识别阶段获取到匹配的单位ID: {unit_ids}")
        else:
            logging.info(f"[参数转换] 意图识别阶段未匹配到单位，使用原有单位提取逻辑")
    except Exception as e:
        logging.warning(f"[参数转换] 从意图识别阶段获取单位信息失败: {str(e)}")
    
    # 如果意图识别阶段没有匹配到单位，使用原有的单位提取逻辑
    if not unit_ids:
        try:
            keyword_result = await extract_settlement_keyword_with_ambiguity_check(user_query, phone_number, history)
            
            # 检查是否为模糊关键词处理结果
            if isinstance(keyword_result, dict) and keyword_result.get('is_ambiguous'):
                # 如果是模糊关键词，直接返回处理结果，让调用方处理反问逻辑
                logging.info(f"[参数转换] 检测到模糊关键词，返回反问消息")
                return {
                    'is_ambiguous_keyword': True,
                    'ambiguous_response': keyword_result
                }
            
            # 如果是明确的关键词，继续原有逻辑
            keyword = keyword_result
            logging.info(f"[参数转换] 提取清算单位关键词: {keyword}")
            unit_ids = await get_unit_id_by_phone_and_keyword(phone_number, keyword, history)
            logging.info(f"[参数转换] 查询到的单位ID: {unit_ids}")
        except HTTPException as e:
            # 如果提取清算单位关键词失败，尝试获取用户的可查询单位列表并使用第一个单位
            if "未从查询中提取到清算单位关键词" in str(e) or "未找到匹配的单位ID" in str(e):
                logging.warning(f"[参数转换] 无法从查询中提取清算单位关键词，尝试使用第一个可查询单位")
                try:
                    available_units = await get_user_available_units(phone_number, return_full_info=True)
                    if available_units and len(available_units) > 0:
                        # 获取第一个单位的ID和名称
                        first_unit = available_units[0]
                        if isinstance(first_unit, dict):
                            unit_id = first_unit.get("unitId")
                            unit_name = first_unit.get("unitFullName", first_unit.get("unitName", "未知单位"))
                        else:
                            unit_id = first_unit
                            unit_name = "未知单位"
                        
                        unit_ids = [unit_id]
                        logging.info(f"[参数转换] 使用第一个可查询单位: {unit_name} (ID: {unit_id})")
                        
                        # 将第一个清算单位名称加入到用户查询中，以便后续处理
                        user_query = f"{unit_name}的{user_query}"
                        logging.info(f"[参数转换] 已将第一个清算单位加入查询: {user_query}")
                    else:
                        # 如果用户没有可查询单位，提供更友好的错误信息
                        if "对数据进行" in user_query or "趋势分析" in user_query or "可视化" in user_query:
                            # 对于分析类查询，提供更具体的错误提示
                            raise HTTPException(status_code=400, detail="进行数据分析需要先指定查询单位。由于您的历史对话中未找到明确的单位信息，请明确指定要分析的单位，或联系管理员绑定单位。")
                        else:
                            raise HTTPException(status_code=400, detail="未找到可查询单位，请检查您的查询条件或联系管理员绑定单位")
                except Exception as inner_e:
                    logging.error(f"[参数转换] 获取可查询单位列表失败: {str(inner_e)}")
                    if "对数据进行" in user_query or "趋势分析" in user_query or "可视化" in user_query:
                        raise HTTPException(status_code=400, detail="进行数据分析需要先指定查询单位。由于无法获取您的单位信息，请明确指定要分析的单位，或联系管理员绑定单位。")
                    else:
                        raise HTTPException(status_code=400, detail="未找到可查询单位，请检查您的查询条件或联系管理员绑定单位")
            else:
                raise e
    
    end_time = time.time()
    logging.info(f"[步骤1-参数转换] 参数转换耗时: {end_time - start_time:.2f}秒")
    param_templates = {
        "invoice": f"请将用户的自然语言需求转换为发票查询接口的JSON参数:\n必须参数：startDate(开始日期YYYYMMDD)、endDate(结束日期YYYYMMDD)\n注意：发票查询参数格式为YYYYMMDD，表示具体的日期。例如：20250615表示2025年6月15日，20250701表示2025年7月1日。\n只返回JSON对象，不要其他文字。（今天是{datetime.now().strftime('%Y年%m月%d日')}）。",
        "sett": f"请将用户的自然语言需求转换为轧差数据查询接口的JSON参数:\n必须参数：startDate(开始日期YYYYMMDD)、endDate(结束日期YYYYMMDD)\n只返回JSON对象，不要其他文字。（今天是{datetime.now().strftime('%Y年%m月%d日')}）。",
        "settDown": f"请将用户的自然语言需求转换为轧差表下载接口的JSON参数:\n必须参数：fileId(文件ID)、tbNo(轧差表编号)\n只返回JSON对象，不要其他文字。（今天是{datetime.now().strftime('%Y年%m月%d日')}）。"
    }

    if param_type not in param_templates:
        raise HTTPException(status_code=400, detail=f"不支持的参数类型: {param_type}")

    prompt = f"{param_templates[param_type]}\n用户需求：{user_query}"

    # 如果有对话历史，添加到prompt中
    if history:
        # 限制历史记录长度，避免提示词过长
        history_str = "\n".join([f"{msg['role']}: {msg['content'][:200]}" for msg in history[-2:]])  # 取最近2条历史，每条限制200字符
        prompt = f"对话历史：\n{history_str}\n\n请参考上述对话历史，理解用户需求的上下文，并{param_templates[param_type]}\n用户需求：{user_query}"
        
        # 对于invoice和sett类型，检查当前查询是否包含明确的时间要求
        if param_type in ["invoice", "sett"]:
            # 检查当前查询是否包含明确的时间关键词
            time_keywords = ['今天', '昨天', '本周', '上周', '本月', '上月', '今年', '去年', 
                           '年', '月', '日', '号', '周', '季度', '上半年', '下半年']
            has_time_keywords = any(keyword in user_query for keyword in time_keywords)
            
            # 检查是否包含日期格式 (YYYY-MM-DD, YYYYMMDD等)
            import re
            date_pattern = r'\d{4}[-/]?\d{1,2}[-/]?\d{1,2}'
            has_date_format = bool(re.search(date_pattern, user_query))
            
            # 只有当当前查询没有明确时间要求时，才从历史中提取时间参数
            if not has_time_keywords and not has_date_format:
                # 从历史中提取时间参数
                time_params = await extract_time_params_from_history(history)
                if time_params.get("confidence", 0) > 0.7:  # 只在置信度较高时使用
                    start_date = time_params.get("startDate")
                    end_date = time_params.get("endDate")
                    if start_date and end_date:
                        # 转换为YYYYMMDD格式
                        start_date_formatted = start_date.replace("-", "")
                        end_date_formatted = end_date.replace("-", "")
                        prompt += f"\n\n注意：当前查询没有明确的时间要求，根据历史对话，用户可能想查询的时间范围是{start_date}至{end_date}，请使用这个时间范围。"

    param_json = await LLMService.call_qwen_model(prompt, timeout=30)
    logging.info(f"[LLM调用] 参数转换结果: {param_json[:100]}...")

    # 清理JSON字符串
    cleaned_json = clean_json_response(param_json)
    
    try:
        params = json.loads(cleaned_json)
        # 验证必要参数
        if param_type in ["invoice", "sett"]:
            if "startDate" not in params or "endDate" not in params:
                raise ValueError(f"缺少必要的时间参数: {cleaned_json}")
            
            # 对于发票查询，处理时间参数格式
            if param_type == "invoice":
                # 检查是否为YYYYMMDD格式（8位数字）
                if len(params["startDate"]) == 8 and len(params["endDate"]) == 8:
                    # 验证是否为纯数字
                    if params["startDate"].isdigit() and params["endDate"].isdigit():
                        # 验证日期格式是否正确
                        try:
                            start_date_obj = datetime.strptime(params["startDate"], "%Y%m%d")
                            end_date_obj = datetime.strptime(params["endDate"], "%Y%m%d")
                            # 日期格式正确，直接使用，无需转换
                            logging.info(f"[日期参数验证] YYYYMMDD格式日期验证通过: {params['startDate']} 至 {params['endDate']}")
                        except ValueError:
                            logging.warning(f"[日期参数验证] YYYYMMDD格式日期验证失败: {params['startDate']} 至 {params['endDate']}")
                            # 如果日期格式不正确，使用默认时间范围
                            today = datetime.now()
                            end_date = today.strftime("%Y%m%d")
                            start_date = (today - timedelta(days=90)).strftime("%Y%m%d")
                            params["startDate"] = start_date
                            params["endDate"] = end_date
                            logging.info(f"[日期参数修正] 使用默认时间参数: {start_date} 至 {end_date}")
                # 检查是否为YYYYMMDD格式（8位数字）
                elif len(params["startDate"]) == 8 and len(params["endDate"]) == 8:
                    # 验证是否为纯数字
                    if params["startDate"].isdigit() and params["endDate"].isdigit():
                        # 验证日期格式是否正确
                        try:
                            start_date_obj = datetime.strptime(params["startDate"], "%Y%m%d")
                            end_date_obj = datetime.strptime(params["endDate"], "%Y%m%d")
                            # 日期格式正确，直接使用，无需转换
                            logging.info(f"[日期参数验证] YYYYMMDD格式日期验证通过: {params['startDate']} 至 {params['endDate']}")
                        except ValueError:
                            logging.warning(f"[日期参数验证] YYYYMMDD格式日期验证失败: {params['startDate']} 至 {params['endDate']}")
                            # 如果日期格式不正确，使用默认时间范围
                            today = datetime.now()
                            end_date = today.strftime("%Y%m01")
                            # 计算3个月前的月份第一天
                            start_year = today.year
                            start_month = today.month - 3
                            if start_month <= 0:
                                start_year -= 1
                                start_month += 12
                            start_date = f"{start_year:04d}{start_month:02d}01"
                            params["startDate"] = start_date
                            params["endDate"] = end_date
                            logging.info(f"[日期参数修正] 使用默认时间参数: {start_date} 至 {end_date}")
                # 检查是否为YYYYMMDD格式（8位数字）
                elif len(params["startDate"]) == 8 and len(params["endDate"]) == 8:
                    # 验证是否为纯数字
                    if params["startDate"].isdigit() and params["endDate"].isdigit():
                        # 验证日期格式是否正确
                        try:
                            start_date_obj = datetime.strptime(params["startDate"], "%Y%m%d")
                            end_date_obj = datetime.strptime(params["endDate"], "%Y%m%d")
                            # 日期格式正确，直接使用，无需转换
                            logging.info(f"[日期参数验证] YYYYMMDD格式日期验证通过: {params['startDate']} 至 {params['endDate']}")
                        except ValueError:
                            logging.warning(f"[日期参数验证] YYYYMMDD格式日期验证失败: {params['startDate']} 至 {params['endDate']}")
                            # 如果日期格式不正确，使用默认时间范围
                            today = datetime.now()
                            end_date = today.strftime("%Y%m%d")
                            start_date = (today - timedelta(days=90)).strftime("%Y%m%d")
                            params["startDate"] = start_date
                            params["endDate"] = end_date
                            logging.info(f"[日期参数修正] 使用默认时间参数: {start_date} 至 {end_date}")
        
        # 添加查询到的unitId
        if not unit_ids:
            raise ValueError("未找到匹配的单位ID，请检查您的查询条件或联系管理员绑定单位")
        
        # 如果有多个匹配的单位ID，要求用户提供更精确的信息
        if len(unit_ids) > 1:
            raise ValueError("找到多个匹配的单位ID，请提供更精确的单位名称或关键词")
        
        params["unitId"] = unit_ids[0]
        # 添加手机号参数
        params["phoneNo"] = phone_number
        return params
    except json.JSONDecodeError as e:
        logging.error(f"[参数转换] JSON解析失败: {str(e)}, 原始内容: {param_json}")
        raise HTTPException(status_code=500, detail=f"模型返回非JSON格式：{param_json}")
    except ValueError as e:
        logging.error(f"[参数转换] 参数验证失败: {str(e)}")
        raise HTTPException(status_code=400, detail=str(e))

# 数据分析工具
# 创建一个优化缓存，最大缓存500个结果，过期时间2小时(7200秒)
analysis_cache = TTLCache(maxsize=500, ttl=7200)

# 通用JSON清理函数
def clean_json_response(json_str: str) -> str:
    """
    清理LLM返回的JSON字符串，移除可能的代码块标记
    
    Args:
        json_str: 原始JSON字符串
        
    Returns:
        清理后的JSON字符串
    """
    cleaned_json = json_str.strip()
    # 移除markdown代码块标记
    cleaned_json = re.sub(r'^```json\s*', '', cleaned_json, flags=re.IGNORECASE)
    cleaned_json = re.sub(r'```\s*$', '', cleaned_json)
    return cleaned_json.strip()

# 从历史对话中提取时间参数
async def extract_time_params_from_history(history: List[Dict[str, Any]]) -> Dict[str, Any]:
    """
    从历史对话中提取时间参数
    
    Args:
        history: 对话历史
        
    Returns:
        包含提取的时间参数的字典
    """
    if not history:
        return {"startDate": None, "endDate": None, "confidence": 0.0}
    
    # 获取当前日期和年份（统一处理，避免重复导入）
    from datetime import datetime, timedelta
    today = datetime.now()
    current_year = today.year
    current_month = today.month
    
    # 获取最近10条历史消息进行分析
    recent_history = history[-10:] if len(history) >= 10 else history
    
    # 检查当前用户查询是否包含相对时间概念
    current_user_query = ""
    for msg in reversed(recent_history):
        if msg.get("role", "") == "user":
            current_user_query = msg.get("content", "")
            break
    
    # 扩展相对时间关键词，包含更多常见表达
    relative_time_keywords = [
        '最近一个月', '最近30天', '最近31天', '最近四周', '最近28天',
        '本月', '这个月', '当前月', '当月',
        '上月', '上个月', '前一个月',
        '本季度', '这个季度', '当前季度',
        '上季度', '上个季度', '前一个季度',
        '今年', '本年', '当前年',
        '去年', '上一年', '前一年'
    ]
    
    # 如果当前查询包含相对时间概念，直接计算时间范围
    if any(keyword in current_user_query for keyword in relative_time_keywords):
        # 根据不同的相对时间关键词计算具体范围
        if any(keyword in current_user_query for keyword in ['最近一个月', '最近30天', '最近31天', '最近四周', '最近28天']):
            # 最近一个月
            end_date = today.strftime("%Y-%m-%d")
            start_date = (today - timedelta(days=30)).strftime("%Y-%m-%d")
        elif any(keyword in current_user_query for keyword in ['本月', '这个月', '当前月', '当月']):
            # 本月
            start_date = today.replace(day=1).strftime("%Y-%m-%d")
            next_month = today.replace(day=28) + timedelta(days=4)  # 确保能获取到下个月
            end_date = (next_month.replace(day=1) - timedelta(days=1)).strftime("%Y-%m-%d")
        elif any(keyword in current_user_query for keyword in ['上月', '上个月', '前一个月']):
            # 上月
            first_day_current_month = today.replace(day=1)
            last_day_prev_month = first_day_current_month - timedelta(days=1)
            start_date = last_day_prev_month.replace(day=1).strftime("%Y-%m-%d")
            end_date = last_day_prev_month.strftime("%Y-%m-%d")
        elif any(keyword in current_user_query for keyword in ['本季度', '这个季度', '当前季度']):
            # 本季度
            current_quarter = (current_month - 1) // 3 + 1
            quarter_start_month = (current_quarter - 1) * 3 + 1
            start_date = today.replace(month=quarter_start_month, day=1).strftime("%Y-%m-%d")
            quarter_end_month = quarter_start_month + 2
            quarter_end_date = today.replace(month=quarter_end_month, day=1) + timedelta(days=31)
            quarter_end_date = quarter_end_date.replace(day=1) - timedelta(days=1)
            end_date = quarter_end_date.strftime("%Y-%m-%d")
        elif any(keyword in current_user_query for keyword in ['上季度', '上个季度', '前一个季度']):
            # 上季度
            prev_quarter = ((current_month - 1) // 3 - 1) % 4 + 1
            quarter_start_month = (prev_quarter - 1) * 3 + 1
            start_date = today.replace(month=quarter_start_month, day=1).strftime("%Y-%m-%d")
            quarter_end_month = quarter_start_month + 2
            quarter_end_date = today.replace(month=quarter_end_month, day=1) + timedelta(days=31)
            quarter_end_date = quarter_end_date.replace(day=1) - timedelta(days=1)
            end_date = quarter_end_date.strftime("%Y-%m-%d")
        elif any(keyword in current_user_query for keyword in ['今年', '本年', '当前年']):
            # 今年
            start_date = today.replace(month=1, day=1).strftime("%Y-%m-%d")
            end_date = today.replace(month=12, day=31).strftime("%Y-%m-%d")
        elif any(keyword in current_user_query for keyword in ['去年', '上一年', '前一年']):
            # 去年
            start_date = today.replace(year=current_year-1, month=1, day=1).strftime("%Y-%m-%d")
            end_date = today.replace(year=current_year-1, month=12, day=31).strftime("%Y-%m-%d")
        else:
            # 默认：最近一个月
            end_date = today.strftime("%Y-%m-%d")
            start_date = (today - timedelta(days=30)).strftime("%Y-%m-%d")
        
        logging.info(f"[时间参数提取] 检测到相对时间查询 '{current_user_query}'，计算时间范围: {start_date} 至 {end_date}")
        return {"startDate": start_date, "endDate": end_date, "confidence": 0.9}
    
    # 构建历史对话文本
    history_text = ""
    for msg in recent_history:
        role = msg.get("role", "")
        content = msg.get("content", "")
        if role == "user":
            history_text += f"用户: {content}\n"
        elif role == "assistant":
            # 只保留助手回复的前200个字符，避免过长
            history_text += f"助手: {content[:200]}...\n"
    
    # 使用LLM从历史对话中提取时间参数
    prompt = f"""
请从以下历史对话中提取时间参数信息。如果找到明确的时间范围，返回JSON格式的startDate和endDate。
如果没有找到明确的时间范围，返回null值。

重要提示：当前日期是{today.strftime('%Y年%m月%d日')}，当前年份是{current_year}年。
请优先考虑当前年份的时间范围，除非历史对话中明确指定了其他年份。

时间范围调整规则：
1. 如果历史对话中提到的时间是过去的年份（如2023年），请将其调整为当前年份{current_year}年
2. 如果历史对话中提到的时间是未来的年份（如2025年），请将其调整为当前年份{current_year}年
3. 如果历史对话中只提到月份（如"1月"、"一月"），请使用当前年份{current_year}年
4. 如果历史对话中只提到季度（如"第一季度"），请使用当前年份{current_year}年
5. 如果历史对话中提到相对时间（如"最近"、"近期"），请基于当前日期{today.strftime('%Y-%m-%d')}计算合理范围

特别说明：当用户询问"最近一个月"、"最近30天"等相对时间概念时，请基于当前日期计算时间范围。

历史对话:
{history_text}

请返回JSON格式的结果，包含以下字段：
- startDate: 开始日期，格式为YYYY-MM-DD，如果没有则返回null
- endDate: 结束日期，格式为YYYY-MM-DD，如果没有则返回null
- confidence: 置信度，0.0-1.0之间的浮点数，表示对提取结果的置信度

示例输出:
{{"startDate": "{current_year}-01-01", "endDate": "{current_year}-01-31", "confidence": 0.8}}
"""
    
    try:
        result = await LLMService.call_qwen_model(prompt, temperature=0.0, timeout=10)
        
        # 尝试解析JSON结果
        import re
        json_match = re.search(r'\{.*\}', result, re.DOTALL)
        if json_match:
            json_str = json_match.group(0)
            time_params = json.loads(json_str)
            
            # 验证日期格式
            if time_params.get("startDate"):
                try:
                    # 验证日期格式
                    from datetime import datetime
                    datetime.strptime(time_params["startDate"], "%Y-%m-%d")
                except (ValueError, TypeError):
                    time_params["startDate"] = None
                    
            if time_params.get("endDate"):
                try:
                    # 验证日期格式
                    from datetime import datetime
                    datetime.strptime(time_params["endDate"], "%Y-%m-%d")
                except (ValueError, TypeError):
                    time_params["endDate"] = None
            
            # 确保confidence在有效范围内
            if "confidence" not in time_params:
                time_params["confidence"] = 0.5
            else:
                try:
                    time_params["confidence"] = float(time_params["confidence"])
                    time_params["confidence"] = max(0.0, min(1.0, time_params["confidence"]))
                except (ValueError, TypeError):
                    time_params["confidence"] = 0.5
                    
            return time_params
        else:
            logging.warning(f"[时间参数提取] 无法从LLM响应中解析JSON: {result}")
            return {"startDate": None, "endDate": None, "confidence": 0.0}
            
    except Exception as e:
        logging.error(f"[时间参数提取] 从历史中提取时间参数失败: {str(e)}")
        return {"startDate": None, "endDate": None, "confidence": 0.0}

# 历史上下文分析缓存
history_context_cache = {}
MAX_CACHE_SIZE = 100  # 最大缓存条目数

# 清理历史上下文缓存
def clear_history_context_cache():
    """清理历史上下文缓存"""
    global history_context_cache
    history_context_cache.clear()
    logging.info("[历史上下文分析] 缓存已清理")

# 限制缓存大小
def limit_cache_size():
    """限制缓存大小，删除最旧的条目"""
    global history_context_cache
    if len(history_context_cache) > MAX_CACHE_SIZE:
        # 删除最旧的条目（简单的FIFO策略）
        keys_to_remove = list(history_context_cache.keys())[:len(history_context_cache) - MAX_CACHE_SIZE]
        for key in keys_to_remove:
            del history_context_cache[key]
        logging.info(f"[历史上下文分析] 缓存大小已限制，删除了{len(keys_to_remove)}个旧条目")

# 分析历史对话上下文
def analyze_history_context(history: List[Dict[str, Any]]) -> Dict[str, Any]:
    """
    分析历史对话上下文，用于更准确的意图识别
    
    Args:
        history: 对话历史
        
    Returns:
        包含历史上下文分析的字典
    """
    if not history:
        return {
            "has_etc_content": False,
            "has_invoice_content": False,
            "has_sett_content": False,
            "recent_query_type": None,
            "dominant_type": None,
            "query_type_counts": {"invoice": 0, "sett": 0, "etc_invoice": 0, "etc_sett": 0},
            "mixed_history": False,
            "context_strength": 0.0,
            "specific_etc_merchant": None  # 新增：具体的ETC商户名称
        }
    
    # 生成缓存键 - 基于最近5条对话的哈希值
    recent_history_for_cache = history[-10:] if len(history) >= 10 else history
    cache_key = hash(str(recent_history_for_cache))
    
    # 检查缓存
    if cache_key in history_context_cache:
        logging.debug(f"[历史上下文分析] 缓存命中，键: {cache_key}")
        return history_context_cache[cache_key]
    
    # 获取最近10轮对话（每轮包含用户和助手消息）
    recent_history = history[-20:] if len(history) >= 20 else history
    
    # 统计不同类型查询的频率
    query_type_counts = {"invoice": 0, "sett": 0, "etc_invoice": 0, "etc_sett": 0}
    has_etc_content = False
    has_invoice_content = False
    has_sett_content = False
    specific_etc_merchant = None  # 新增：具体的ETC商户名称
    
    # 常见的ETC商户名称关键词（可以根据实际情况扩展）
    etc_merchant_keywords = [
        '东方体育中心', '上海体育场', '上海体育馆', '体育中心', '体育场', '体育馆',
        '停车场', '停车中心', '停车库', '停车广场', '停车大楼', '停车区域'
    ]
    
    # 分析历史对话内容
    for msg in recent_history:
        content = msg.get("content", "")
        content_lower = content.lower()
        role = msg.get("role", "")
        
        # 检查是否包含ETC相关内容 - 增强匹配准确性
        if any(keyword in content_lower for keyword in ['etc', '停车场', '停车费', '停车结算', '停车交易', '厂库', '厂库发票', '厂库开票', 'etc结算', 'etc发票']) and not any(keyword in content_lower for keyword in ['etc怎么', 'etc如何', 'etc流程']):
            has_etc_content = True
            
            # 新增：尝试识别具体的ETC商户名称
            for merchant_keyword in etc_merchant_keywords:
                if merchant_keyword in content:
                    specific_etc_merchant = merchant_keyword
                    logging.info(f"[历史上下文分析] 识别到具体ETC商户: {specific_etc_merchant}")
                    break
        
        # 检查是否包含发票相关内容 - 增强匹配准确性
        if any(keyword in content_lower for keyword in ['发票', '开票', '票据']) and not any(keyword in content_lower for keyword in ['怎么开票', '如何开票', '开票流程', '开票方法']):
            has_invoice_content = True
        
        # 检查是否包含结算相关内容 - 增强匹配准确性
        if any(keyword in content_lower for keyword in ['结算', '轧差', '结算单', '轧差表']) and not any(keyword in content_lower for keyword in ['怎么结算', '如何结算', '结算流程', '结算方法']):
            has_sett_content = True
        
        # 统计查询类型（只统计用户查询）- 增强匹配准确性
        if role == "user":
            # 增强ETC相关查询的识别
            if any(keyword in content_lower for keyword in ['etc', '停车场', '停车费', '停车交易', '厂库', '厂库发票', '厂库开票']) and not any(keyword in content_lower for keyword in ['etc怎么', 'etc如何', 'etc流程']):
                if any(keyword in content_lower for keyword in ['发票', '开票', '票据']) and not any(keyword in content_lower for keyword in ['怎么开票', '如何开票', '开票流程', '开票方法']):
                    query_type_counts["etc_invoice"] += 1
                elif any(keyword in content_lower for keyword in ['结算', '轧差', '结算单', '轧差表']) and not any(keyword in content_lower for keyword in ['怎么结算', '如何结算', '结算流程', '结算方法']):
                    query_type_counts["etc_sett"] += 1
            # 增强发票相关查询的识别
            elif any(keyword in content_lower for keyword in ['发票', '开票', '票据']) and not any(keyword in content_lower for keyword in ['怎么开票', '如何开票', '开票流程', '开票方法']):
                query_type_counts["invoice"] += 1
            # 增强结算相关查询的识别
            elif any(keyword in content_lower for keyword in ['结算', '轧差', '结算单', '轧差表']) and not any(keyword in content_lower for keyword in ['怎么结算', '如何结算', '结算流程', '结算方法']):
                query_type_counts["sett"] += 1
    
    # 确定最近查询类型（最近一次用户查询）- 增强匹配准确性
    recent_query_type = None
    for msg in reversed(recent_history):
        if msg.get("role", "") == "user":
            content = msg.get("content", "").lower()
            # 增强ETC相关查询的识别
            if any(keyword in content for keyword in ['etc', '停车场', '停车费', '停车交易', '厂库', '厂库发票', '厂库开票']) and not any(keyword in content for keyword in ['etc怎么', 'etc如何', 'etc流程']):
                if any(keyword in content for keyword in ['发票', '开票', '票据']) and not any(keyword in content for keyword in ['怎么开票', '如何开票', '开票流程', '开票方法']):
                    recent_query_type = "etc_invoice"
                    break
                elif any(keyword in content for keyword in ['结算', '轧差', '结算单', '轧差表']) and not any(keyword in content for keyword in ['怎么结算', '如何结算', '结算流程', '结算方法']):
                    recent_query_type = "etc_sett"
                    break
            # 增强发票相关查询的识别
            elif any(keyword in content for keyword in ['发票', '开票', '票据']) and not any(keyword in content for keyword in ['怎么开票', '如何开票', '开票流程', '开票方法']):
                recent_query_type = "invoice"
                break
            # 增强结算相关查询的识别
            elif any(keyword in content for keyword in ['结算', '轧差', '结算单', '轧差表']) and not any(keyword in content for keyword in ['怎么结算', '如何结算', '结算流程', '结算方法']):
                recent_query_type = "sett"
                break
    
    # 确定主导类型（历史中最常出现的查询类型）
    dominant_type = max(query_type_counts.items(), key=lambda x: x[1])[0] if sum(query_type_counts.values()) > 0 else None
    
    # 判断是否是混合历史（多种类型查询都存在）
    non_zero_types = sum(1 for count in query_type_counts.values() if count > 0)
    mixed_history = non_zero_types > 1
    
    # 计算上下文强度（基于最近查询的频率和新鲜度）
    context_strength = 0.0
    if recent_query_type:
        # 最近查询类型权重最高
        context_strength += 0.4
    if dominant_type and query_type_counts[dominant_type] > 1:
        # 主导类型权重
        context_strength += 0.3 * min(query_type_counts[dominant_type] / 5.0, 1.0)
    if not mixed_history:
        # 非混合历史权重
        context_strength += 0.3
    
    result = {
        "has_etc_content": has_etc_content,
        "has_invoice_content": has_invoice_content,
        "has_sett_content": has_sett_content,
        "recent_query_type": recent_query_type,
        "dominant_type": dominant_type,
        "query_type_counts": query_type_counts,
        "mixed_history": mixed_history,
        "context_strength": min(context_strength, 1.0),  # 确保不超过1.0
        "specific_etc_merchant": specific_etc_merchant  # 新增：具体的ETC商户名称
    }
    
    # 存入缓存
    history_context_cache[cache_key] = result
    logging.debug(f"[历史上下文分析] 结果已缓存，键: {cache_key}")
    
    # 限制缓存大小
    limit_cache_size()
    
    return result

async def analyze_data(data: dict, analysis_type: str, start_date: Optional[str] = None, end_date: Optional[str] = None) -> str:
    # 生成缓存键
    cache_key = f"{analysis_type}:{hash(str(data))}:{start_date}:{end_date}"
    
    # 检查是否为监控请求，通过检查数据中是否包含监控标识
    is_monitor = False
    if isinstance(data, dict):
        # 检查数据中的字符串字段是否包含监控标识
        for key, value in data.items():
            if isinstance(value, str) and "[MONITOR_" in value:
                is_monitor = True
                break
    
    # 尝试从缓存获取结果（监控请求不使用缓存）
    if not is_monitor and cache_key in analysis_cache:
        logging.info(f"[步骤4-数据分析] 数据分析缓存命中")
        return analysis_cache[cache_key]
    
    if is_monitor:
        logging.info(f"[步骤4-数据分析] 检测到监控请求，绕过缓存机制")
    
    start_time = time.time()
    logging.info(f"[步骤4-数据分析] 开始数据分析")
    analysis_templates = {
        "invoice": "针对用户查询时间段({start_date}至{end_date})给出简洁的数据分析:主要对含税金额进行分析:1.主要趋势分析2.显著性分析(如果有)：内容限定为方差分析，同比环比等，禁止出现太专业的统计分析术语.3.给出建议。尽可能简短.用markdown格式返回分析报告,报告中的数据不许含有千位分隔符,需要注意\"分\"和\"元\"的转换,报告中金额类的数据单位都是分，需要除以一百才能转换成元,以分为单位的数据不显示出来，只显示单位为元的转换后数据,将数据进行计算时也要记得将金额类数据除以一百转换成元才行,使用 ### （h3）作为主标题。",
        "sett": "针对用户查询时间段({start_date}至{end_date})给出简洁的数据分析：主要对合计消费金额进行分析:1.主要趋势分析2.显著性分析(如果有)：内容限定为方差分析，同比环比等，禁止出现太专业的统计分析术语.3.给出建议。尽可能简短.用markdown格式返回分析报告,报告中的数据不许含有千位分隔符,需要注意\"分\"和\"元\"的转换,报告中金额类的数据单位都是分，需要除以一百才能转换成元,以分为单位的数据不显示出来，只显示单位为元的转换后数据,将数据进行计算时也要记得将金额类数据除以一百转换成元才行,使用 ### （h3）作为主标题。",
        "etc_invoice": "针对用户查询时间段({start_date}至{end_date})给出简洁的数据分析:主要对价税合计金额进行分析:1.主要趋势分析2.显著性分析(如果有)：内容限定为方差分析，同比环比等，禁止出现太专业的统计分析术语.3.给出建议。尽可能简短.用markdown格式返回分析报告,报告中的数据不许含有千位分隔符,需要注意\"分\"和\"元\"的转换,报告中金额类的数据单位都是分，需要除以一百才能转换成元,以分为单位的数据不显示出来，只显示单位为元的转换后数据,将数据进行计算时也要记得将金额类数据除以一百转换成元才行,使用 ### （h3）作为主标题。",
        "etc_sett": "针对用户查询时间段({start_date}至{end_date})给出简洁的数据分析:主要对交易金额进行分析:1.主要趋势分析2.显著性分析(如果有)：内容限定为方差分析，同比环比等，禁止出现太专业的统计分析术语.3.给出建议。尽可能简短.用markdown格式返回分析报告,报告中的数据不许含有千位分隔符,需要注意\"分\"和\"元\"的转换,报告中金额类的数据单位都是分，需要除以一百才能转换成元,以分为单位的数据不显示出来，只显示单位为元的转换后数据,将数据进行计算时也要记得将金额类数据除以一百转换成元才行,使用 ### （h3）作为主标题。"
    }
    prompt = f"{analysis_templates.get(analysis_type)}\n数据：{json.dumps(data, ensure_ascii=False)}"
    if start_date and end_date:
        prompt = prompt.replace("{start_date}", start_date).replace("{end_date}", end_date)
    logging.info(f"[LLM调用] 数据分析请求成功")
    result = await LLMService.call_qwen_model(prompt, temperature=0.0, timeout=60)
    end_time = time.time()
    logging.info(f"[步骤4-数据分析] 数据分析耗时: {end_time - start_time:.2f}秒")
    
    # 将结果存入缓存（监控请求不缓存）
    if not is_monitor:
        analysis_cache[cache_key] = result
    
    return result

# 基于用户资源的意图识别工具
async def detect_intent_by_user_resources(user_query: str, phone_number: str, history: Optional[List[Dict[str, Any]]] = None) -> str:
    """
    基于用户绑定的清算单位和商户列表进行意图识别
    简化逻辑：优先正常提取单位名和意图，无法确定时再结合历史上下文
    
    Args:
        user_query: 用户查询
        phone_number: 用户手机号
        history: 对话历史
        
    Returns:
        意图类型
    """
    start_time = time.time()
    logging.info(f"[步骤0-基于用户资源的意图识别] 开始意图识别")
    
    # 特殊意图检查：available_units和agent_capabilities
    user_query_lower = user_query.lower()
    if any(keyword in user_query_lower for keyword in ['能查询哪些单位', '可以查询哪些单位', '可查询的单位', '可查询单位', '查询哪些单位', '能查哪些单位', '可查哪些单位', '可查什么单位', '能查什么单位', '能问哪些单位', '可以问哪些单位', '能问什么单位']):
        return "available_units"
    
    if any(keyword in user_query_lower for keyword in ['你能帮我干什么', '你能做什么', '你有什么功能', '我能问什么', '我可以问什么', '我可以干嘛', '我能干嘛', '我能干啥', '我可以干啥']):
        return "agent_capabilities"
    
    try:
        # 获取用户绑定的清算单位列表
        available_units = []
        try:
            available_units = await get_user_available_units(phone_number)
        except Exception as e:
            logging.warning(f"[基于用户资源的意图识别] 获取清算单位列表失败: {str(e)}")
            # 继续执行，不抛出异常
        
        # 获取用户绑定的商户列表
        available_merchants = []
        try:
            merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
            if merchants:
                available_merchants = [safe_extract_merchant_name(merchant) for merchant in merchants]
        except Exception as e:
            logging.warning(f"[基于用户资源的意图识别] 获取ETC商户列表失败: {str(e)}")
        
        # 提取用户查询中的关键词
        # 使用LLM提取单位关键词
        unit_prompt = f"""从用户查询中提取可能匹配清算单位或商户名称的关键词。
用户查询：{user_query}

只返回提取的关键词列表，每行一个，不要添加其他文字。如果没有找到关键词，返回"无"。"""
        
        unit_keywords = []
        try:
            unit_result = await LLMService.call_qwen_model(unit_prompt, timeout=30)
            if unit_result.strip() and unit_result.strip() != "无":
                unit_keywords = [kw.strip() for kw in unit_result.strip().split('\n') if kw.strip()]
        except Exception as e:
            logging.error(f"[基于用户资源的意图识别] 提取单位关键词失败: {str(e)}")
        
        # 判断查询类型关键词 - 发票和轧差表需要关键词判断
        is_invoice_query = any(keyword in user_query_lower for keyword in ['发票', '开票', '票据']) and not any(keyword in user_query_lower for keyword in ['怎么开票', '如何开票', '开票流程', '开票方法'])
        is_sett_query = any(keyword in user_query_lower for keyword in ['结算', '轧差', '结算单', '轧差表']) and not any(keyword in user_query_lower for keyword in ['怎么结算', '如何结算', '结算流程', '结算方法'])
        is_analysis_query = any(keyword in user_query_lower for keyword in ['分析', '统计', '趋势', '图表', '可视化', '数据']) and not any(keyword in user_query_lower for keyword in ['怎么分析', '如何分析', '分析方法'])
        
        # 匹配单位关键词 - 同时检查清算单位和商户
        matched_units = []  # 存储所有匹配的清算单位
        matched_merchants = []  # 存储所有匹配的商户
        
        if unit_keywords:
            # 检查是否匹配清算单位
            for unit in available_units:
                for keyword in unit_keywords:
                    if keyword.lower() in unit.lower() or unit.lower() in keyword.lower():
                        matched_units.append(unit)
                        break
            
            # 检查是否匹配商户
            for merchant in available_merchants:
                for keyword in unit_keywords:
                    if keyword.lower() in merchant.lower() or merchant.lower() in keyword.lower():
                        matched_merchants.append(merchant)
                        break
        
        # 第四步：根据匹配结果和查询类型确定意图
        # 清算单位走普通路由（invoice/sett），商户走ETC路由（etc_invoice/etc_sett）
        
        # 1. 优先处理清算单位匹配
        if matched_units:
            logging.info(f"[基于用户资源的意图识别] 匹配到清算单位({matched_units})，走普通路由")
            if is_invoice_query:
                return "invoice"
            elif is_sett_query or is_analysis_query:
                return "sett"
            else:
                # 默认为轧差查询
                return "sett"
        
        # 2. 其次处理商户匹配
        elif matched_merchants:
            logging.info(f"[基于用户资源的意图识别] 匹配到商户({matched_merchants})，走ETC路由")
            if is_invoice_query:
                return "etc_invoice"
            elif is_sett_query or is_analysis_query:
                return "etc_sett"
            else:
                # 默认为ETC轧差查询
                return "etc_sett"
        
        # 3. 如果明确包含ETC关键词，走ETC路由
        elif any(keyword in user_query_lower for keyword in ['etc', '停车场', '停车费', '停车结算', '停车交易', '厂库', '厂库发票', '厂库开票']):
            logging.info(f"[基于用户资源的意图识别] 查询包含ETC关键词，走ETC路由")
            if is_invoice_query:
                return "etc_invoice"
            else:
                return "etc_sett"
        
        # 4. 最后才结合历史上下文进行意图识别
        else:
            logging.info(f"[基于用户资源的意图识别] 未匹配到单位，结合历史上下文进行意图识别")
            return await detect_intent(user_query, history)
            
    except Exception as e:
        logging.error(f"[基于用户资源的意图识别] 错误: {str(e)}")
        # 出错时回退到原有的意图识别逻辑
        return await detect_intent(user_query, history)

# 意图识别工具
async def detect_intent(user_query: str, history: Optional[List[Dict[str, Any]]] = None) -> str:
    start_time = time.time()
    logging.info(f"[步骤0-意图识别] 开始意图识别")
    
    # 构建包含历史对话的提示
    history_context = ""
    is_etc_in_history = False
    history_analysis = None
    specific_etc_merchant = None
    if history:
        # 使用新的历史上下文分析函数
        history_analysis = analyze_history_context(history)
        is_etc_in_history = history_analysis["has_etc_content"]
        specific_etc_merchant = history_analysis["specific_etc_merchant"]
        
        # 构建历史对话上下文
        recent_history = history[-10:] if len(history) >= 10 else history
        history_context = "\n以下是最近的对话历史，用于更好地理解当前问题的上下文：\n"
        for msg in recent_history:
            history_context += f"{msg['role']}: {msg['content']}\n"
        
        # 新增：如果识别到具体的ETC商户，在历史上下文前添加特殊提示
        if specific_etc_merchant:
            history_context = f"\n重要提示：历史对话中提到了具体的ETC商户 '{specific_etc_merchant}'，当前分析类查询应继承此ETC商户上下文。\n" + history_context
    
    prompt = f"判断用户问题的意图类别，只能返回以下七种之一：invoice、sett、etc_invoice、etc_sett、dify、available_units、agent_capabilities。判断规则：\n"
    prompt += f"- 'invoice': 用户需要获取或查询具体的发票数据信息，如'查询某段时间的发票'、'发票金额分析'、'发票数据统计'、'查询对应的电子发票'等直接数据请求，包括生成发票相关的图表、趋势图、可视化分析\n"
    prompt += f"- 'sett': 用户需要获取或查询具体的轧差结算数据信息，如'查询轧差数据'、'结算单信息'、'消费金额统计'、'查询结算表'、'查询结算数据'、'查询对账单'、'查询报表'等直接数据请求，包括生成轧差相关的图表、趋势图、可视化分析（注意：'结算数据'和'轧差'是同一个意思）。特别地，当用户询问轧差表中的具体数据字段时，如'卡消费调整金额'、'卡消费金额'、'充资金额'、'退卡金额'、'手续费'、'换乘优惠'、'预付款'等，也应判断为'sett'意图，因为这些数据都需要通过查询轧差表获取\n"
    prompt += f"- 'etc_invoice': 用户需要获取或查询具体的停车场发票数据信息，如'查询停车场发票'、'停车场发票金额分析'、'停车场发票数据统计'、'查询停车场电子发票'等直接数据请求，包括生成停车场发票相关的图表、趋势图、可视化分析\n"
    prompt += f"- 'etc_sett': 用户需要获取或查询具体的停车场结算数据信息，如'查询停车场结算数据'、'查询停车场轧差数据'、'停车场结算数据分析'、'停车场交易金额统计'等直接数据请求，包括生成停车场结算相关的图表、趋势图、可视化分析\n"
    prompt += f"- 'available_units': 用户询问可以查询哪些单位或停车场(厂库，商户)，如与'能查询哪些单位'、'可以查询哪些单位'、'可查询的单位'、'可查询单位'、'查询哪些单位'、'能查哪些单位'、'可查哪些单位'、'可查什么单位'、'能查什么单位'、'能问哪些单位'、'可以问哪些单位'、'能问什么单位'等类似的问题\n"
    prompt += f"- 'agent_capabilities': 用户询问智能体能提供什么功能、能帮助做什么，如与'你能帮我干什么'、'你能做什么'、'你有什么功能'、'我能问什么'、'我可以问什么'、'我可以干嘛'、'我能干嘛'、'我能干啥'、'我可以干啥'等类似的问题\n"
    prompt += f"- 'dify': 用户询问如何操作、流程步骤、业务知识、规则解释等问题，如'怎么下载轧差表'、'轧差表如何生成'、'发票处理流程'、'交通卡清算可疑账类型'等知识咨询\n\n"
    prompt += f"特别注意：\n"
    prompt += f"1. 如果用户请求中包含'画图'、'图表'、'趋势图'、'可视化'、'统计图'、'数据分析'、'数据可视化'、'生成图表'、'画个图'、'做个图'等关键词，即使没有明确指出业务类型，也应优先判断为对应的数据查询意图（invoice、sett、etc_invoice或etc_sett），而不是dify。如果包含图表关键词但没有明确业务类型，请根据对话历史判断最近的业务主题，如果无法判断则默认为sett。\n"
    prompt += f"2. 如果用户请求中包含'能查询哪些单位'、'可以查询哪些单位'、'可查询的单位'、'可查询单位'、'查询哪些单位'、'能查哪些单位'、'可查哪些单位'、'可查什么单位'、'能查什么单位'、'能问哪些单位'、'可以问哪些单位'、'能问什么单位'等关键词，应直接判断为'available_units'。\n"
    prompt += f"3. 如果用户请求中包含'可视化'、'分析'、'图表'、'趋势'、'统计'、'数据'、'报告'等模糊查询关键词，请结合对话历史来判断用户的具体意图。如果历史对话中提到了特定的数据类型（如发票、轧差、ETC发票、ETC结算等），则应优先判断为对应的数据查询意图。如果无法区分是发票还是轧差表，请优先选择轧差表（sett）。\n"
    prompt += f"4. 即使问题中包含发票、轧差、ETC等关键词，但如果用户询问的是'怎么'、'如何'、'为什么'、'流程'、'方法'、'什么时候'、'何时'、'多久'、'时间'等操作方式、知识解释或时间相关询问，应归类为dify而不是invoice、sett、etc_invoice或etc_sett。\n"
    prompt += f"5. 如果用户请求中包含'停车场'、'停车费'、'停车结算'、'停车交易'等与停车场相关的关键词，应直接判断为'etc_sett'。\n"
    prompt += f"6. 如果用户请求中包含'厂库'、'厂库发票'、'厂库开票'等与厂库相关的关键词，应直接判断为'etc_invoice'。\n"
    prompt += f"7. 特别注意：当用户询问轧差表中的具体数据字段时，如'卡消费调整金额'、'卡消费金额'、'充资金额'、'退卡金额'、'结算单位号'、'报表编号'、'总笔数'、'总金额'等，即使没有明确提到'轧差表'或'结算'，也应判断为'sett'意图，因为这些数据都来源于轧差表。\n"
    prompt += f"8. 关键改进：当用户请求是分析类查询（如'分析一下'、'趋势分析'、'可视化'等）且没有明确指定单位时，必须结合历史对话上下文来判断意图。如果上一个问题是关于ETC商户（如'东方体育中心'、'停车场'等）的轧差表查询，当前分析请求应继承相同的ETC商户上下文，判断为'etc_sett'意图。\n"
    prompt += f"9. 关键改进：当历史对话中明确提到了具体的ETC商户名称（如'东方体育中心'、'上海体育场'等）或停车场相关关键词，且当前用户请求是分析类查询时，应优先判断为'etc_sett'意图，以保持对话上下文的连贯性。\n\n"
    
    prompt += f"{history_context}\n用户问题：{user_query}\n"
    prompt += "请直接返回类别名称，不要添加任何额外文字或解释。"

    try:
        result = (await LLMService.call_qwen_model(prompt, timeout=30)).strip()
        # 确保结果不为空
        if not result:
            logging.error(f"[意图识别] 大模型返回空结果，默认使用dify意图")
            end_time = time.time()
            logging.info(f"[步骤0-意图识别] 意图识别耗时: {end_time - start_time:.2f}秒")
            return "dify"
        
        result_lower = result.lower()
        logging.info(f"[意图识别] 识别结果: {result_lower}") 
        end_time = time.time()
        logging.info(f"[步骤0-意图识别] 意图识别耗时: {end_time - start_time:.2f}秒")
        
        # 不再进行ETC意图修正，完全依赖detect_intent_by_user_resources函数的单位列表匹配逻辑
        # ETC意图识别应完全基于单位列表匹配，而非历史上下文或关键词
        
        # 确保返回的是有效的意图类型
        if result_lower in ["dify", "invoice", "sett", "etc_invoice", "etc_sett", "available_units", "agent_capabilities"]:
            return result_lower
        else:
            logging.warning(f"[意图识别] 识别到无效意图: {result_lower}，默认使用dify意图")
            return "dify"
    except Exception as e:
        logging.error(f"意图识别错误: {str(e)}, query: {user_query}")
        return "dify"

async def route_request(user_query: str, phone_number: str, debug: bool = False, conversation_id: Optional[str] = None, include_analysis: Optional[bool] = None, include_chart: Optional[bool] = None, timeout: int = 60, is_monitor: bool = False) -> Dict[str, Any]:
    start_time = time.time()
    logging.info(f"[步骤0-请求路由] 开始请求路由")
    
    # 记录用户提问日志
    try:
        await log_user_query(user_query, phone_number, conversation_id)
    except Exception as e:
        logging.error(f"[用户提问日志] 记录用户提问失败: {str(e)}")
    
    # 验证手机号格式（使用公共函数）
    validate_phone_number(phone_number)
    
    # 获取对话历史
    history = chat_history_manager.get_history(conversation_id) if conversation_id else []
    intent = await detect_intent_by_user_resources(user_query, phone_number, history)
    logging.info(f"[请求路由] 路由到类别: {intent}") 
    
    # 特殊处理：处理"我能查询哪些单位"请求
    if intent == "available_units":
        try:
            # 调用API获取用户可查询的单位列表
            available_units = []
            try:
                available_units = await get_user_available_units(phone_number)
            except HTTPException as e:
                # 如果是404错误（未查询到清算单位），记录日志但继续执行
                if e.status_code == 404:
                    logging.warning(f"[查询单位列表] 用户无清算单位: {str(e)}")
                else:
                    # 其他错误直接抛出
                    raise e
            
            # 调用API获取用户可查询的ETC商户列表
            available_merchants = []
            try:
                merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
                if merchants:
                    available_merchants = [safe_extract_merchant_name(merchant) for merchant in merchants]
            except Exception as e:
                logging.warning(f"[查询单位列表] 获取ETC商户列表失败: {str(e)}")
            
            # 构建回复消息
            if available_units or available_merchants:
                answer_parts = []
                
                if available_units:
                    units_str = "\n".join([f"- {unit}" for unit in available_units])
                    answer_parts.append(f"### 结算单位\n{units_str}")
                
                if available_merchants:
                    merchants_str = "\n".join([f"- {merchant}" for merchant in available_merchants])
                    answer_parts.append(f"### 商户\n{merchants_str}")
                
                answer = "您当前可以查询的单位与商户有：\n\n" + "\n\n".join(answer_parts) + "\n\n您可以直接询问这些单位的发票或轧差结算相关数据，以及商户的ETC发票或结算数据。"
            else:
                answer = "您的微信手机号未绑定到任何清算单位或商户，请联系您的客户经理，完成绑定后可使用本清算智能体的功能。"
            
            # 保存对话记录（当前用户查询和响应）
            if conversation_id:
                chat_history_manager.add_message(conversation_id, "user", user_query)
                chat_history_manager.add_message(conversation_id, "assistant", answer)
            
            return {
                "type": "available_units",
                "result": {
                    "answer": answer,
                    "conversation_id": conversation_id
                },
                "conversation_id": conversation_id,
                "raw_data": None if not debug else {
                    "user_query": user_query,
                    "conversation_id": conversation_id,
                    "available_units": available_units,
                    "available_merchants": available_merchants
                }
            }
        except Exception as e:
            error_msg = str(e) if str(e) else "查询单位列表失败，请稍后重试"
            logging.error(f"[查询单位列表] 错误: {error_msg}")
            
            # 保存对话记录（当前用户查询和错误响应）
            if conversation_id:
                chat_history_manager.add_message(conversation_id, "user", user_query)
                chat_history_manager.add_message(conversation_id, "assistant", error_msg)
            
            return {
                "type": "error",
                "result": {
                    "answer": error_msg,
                    "conversation_id": conversation_id
                },
                "conversation_id": conversation_id,
                "raw_data": None if not debug else {
                    "user_query": user_query,
                    "conversation_id": conversation_id,
                    "error": error_msg
                }
            }
    
    # 特殊处理：处理"你能帮我干什么"请求
    if intent == "agent_capabilities":
        try:
            # 获取用户可查询的单位列表
            available_units = []
            if phone_number:
                try:
                    available_units = await get_user_available_units(phone_number)
                except HTTPException as e:
                    # 如果是404错误（未查询到清算单位），记录日志但继续执行
                    if e.status_code == 404:
                        logging.warning(f"[智能体功能描述] 用户无清算单位: {str(e)}")
                    else:
                        # 其他错误直接抛出
                        raise e
            
            # 获取用户可查询的ETC商户列表
            available_merchants = []
            if phone_number:
                try:
                    merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
                    if merchants:
                        available_merchants = [safe_extract_merchant_name(merchant) for merchant in merchants]
                except Exception as e:
                    logging.warning(f"[智能体功能描述] 获取ETC商户列表失败: {str(e)}")
            
            # 构建智能体功能描述
            capabilities = """我是您的智能助手，可以为您提供以下服务：

### 数据查询功能
- **发票查询**：查询您绑定的单位的发票数据，包括发票金额、开票日期等信息
- **轧差结算查询**：查询您绑定的单位的轧差结算数据，包括消费金额、结算单信息等
- **ETC发票查询**：查询您绑定的商户的ETC发票数据，包括开票金额、开票状态等
- **ETC结算查询**：查询您绑定的商户的ETC结算数据，包括交易金额、交易笔数等

### 数据分析功能
- **趋势分析**：对您的结算数据进行趋势分析，帮助您了解业务变化
- **图表生成**：将数据以趋势图形式展示

### 辅助功能
- **单位查询**：查询您可以访问的单位列表
- **业务咨询**：解答您关于发票处理、轧差结算等清算业务的操作流程和规则问题
"""
            
            # 如果有可查询的单位或商户，添加到功能描述中
            if available_units or available_merchants:
                capabilities += "\n\n### 您当前可以查询的单位与商户"
                
                if available_units:
                    units_str = "\n".join([f"- {unit}" for unit in available_units])
                    capabilities += f"\n\n#### 结算单位\n{units_str}"
                
                if available_merchants:
                    merchants_str = "\n".join([f"- {merchant}" for merchant in available_merchants])
                    capabilities += f"\n\n#### 商户\n{merchants_str}"
            else:
                capabilities += "\n\n### 单位绑定\n您的微信手机号未绑定到任何清算单位或商户，请联系您的客户经理，完成绑定后可使用本清算智能体的功能。"
            
            # 保存对话记录（当前用户查询和响应）
            if conversation_id:
                chat_history_manager.add_message(conversation_id, "user", user_query)
                chat_history_manager.add_message(conversation_id, "assistant", capabilities)
            
            return {
                "type": "agent_capabilities",
                "result": {
                    "answer": capabilities,
                    "conversation_id": conversation_id
                },
                "conversation_id": conversation_id,
                "raw_data": None if not debug else {
                    "user_query": user_query,
                    "conversation_id": conversation_id,
                    "available_units": available_units,
                    "available_merchants": available_merchants
                }
            }
        except Exception as e:
            error_msg = str(e) if str(e) else "获取智能体功能描述失败，请稍后重试"
            logging.error(f"[智能体功能描述] 错误: {error_msg}")
            
            # 保存对话记录（当前用户查询和错误响应）
            if conversation_id:
                chat_history_manager.add_message(conversation_id, "user", user_query)
                chat_history_manager.add_message(conversation_id, "assistant", error_msg)
            
            return {
                "type": "error",
                "result": {
                    "answer": error_msg,
                    "conversation_id": conversation_id
                },
                "conversation_id": conversation_id,
                "raw_data": None if not debug else {
                    "user_query": user_query,
                    "conversation_id": conversation_id,
                    "error": error_msg
                }
            }
    
    # 当问数分类（invoice、sett、etc_invoice或etc_sett）缺少必要参数（时间和unitid）时，转到问知（dify）
    if intent not in ["dify"]:
        try:
            # 尝试转换参数，检查是否缺少必要的时间和unitid参数
            params = await convert_params(user_query, intent, phone_number, history)
            logging.info(f"[请求路由] {intent}类别参数验证通过")
        except HTTPException as e:
            # 检查是否是单位未绑定的特定错误
            if e.detail == "您需要前往统一对账平台进行绑定后才能查询" or "未找到与关键词" in e.detail or "您没有权限查询此单位" in e.detail or "未找到关联商户，请先绑定商户" in e.detail:
                # 直接返回错误信息，不转到Dify
                return {
                    "type": "error",
                    "result": {
                        "answer": e.detail,
                        "conversation_id": conversation_id
                    },
                    "conversation_id": conversation_id,
                    "raw_data": None if not debug else {
                        "user_query": user_query,
                        "conversation_id": conversation_id,
                        "error": e.detail
                    }
                }
            else:
                # 对于sett和invoice意图，参数转换失败时使用混合服务进行自动切换
                if intent in ["sett", "invoice"]:
                    logging.info(f"[请求路由] {intent}类别参数转换失败，将使用混合服务进行自动切换，错误：{str(e)}")
                    # 不改变intent，保持原意图，让后续的混合服务处理逻辑接管
                else:
                    # 对于etc_invoice和etc_sett意图，参数转换失败时转到dify
                    logging.info(f"[请求路由] {intent}类别缺少必要参数（时间或unitid），转到问知（dify），错误：{str(e)}")
                    intent = "dify"
        except Exception as e:
            # 对于sett和invoice意图，参数转换失败时使用混合服务进行自动切换
            if intent in ["sett", "invoice"]:
                logging.info(f"[请求路由] {intent}类别参数转换失败，将使用混合服务进行自动切换，错误：{str(e)}")
                # 不改变intent，保持原意图，让后续的混合服务处理逻辑接管
            else:
                # 对于etc_invoice和etc_sett意图，参数转换失败时转到dify
                logging.info(f"[请求路由] {intent}类别缺少必要参数（时间或unitid），转到问知（dify），错误：{str(e)}")
                intent = "dify"
    
    # 根据意图类型路由到不同的处理函数
    if intent == "dify":
        result = await _handle_dify(user_query, debug, conversation_id, timeout, is_monitor)
    elif intent == "invoice":
        # 处理发票查询
        # 尝试提取参数，如果成功则传递给发票服务
        try:
            params = await convert_params(user_query, "invoice", phone_number, history)
            result = await _handle_invoice(user_query, debug, phone_number, conversation_id, include_analysis, include_chart, timeout, is_monitor, params=params)
        except Exception as e:
            # 如果参数提取失败，仍然调用发票服务，但不传递params参数，让服务内部自动提取
            logging.warning(f"[请求路由] invoice类别参数提取失败，将使用自动参数提取: {str(e)}")
            result = await _handle_invoice(user_query, debug, phone_number, conversation_id, include_analysis, include_chart, timeout, is_monitor)
    elif intent == "sett":
        # 处理轧差结算查询
        # 尝试提取参数，如果成功则传递给轧差结算服务
        try:
            params = await convert_params(user_query, "sett", phone_number, history)
            result = await _handle_sett(user_query, debug, phone_number, conversation_id, include_analysis, include_chart, timeout, is_monitor, params=params)
        except Exception as e:
            # 如果参数提取失败，仍然调用轧差结算服务，但不传递params参数，让服务内部自动提取
            logging.warning(f"[请求路由] sett类别参数提取失败，将使用自动参数提取: {str(e)}")
            result = await _handle_sett(user_query, debug, phone_number, conversation_id, include_analysis, include_chart, timeout, is_monitor)
    elif intent == "etc_invoice":
        # 处理可能为 None 的参数
        safe_conversation_id = conversation_id or ""
        # 尝试提取参数，如果成功则传递给ETC发票服务
        try:
            params = await convert_params(user_query, "etc_invoice", phone_number, history)
            result = await _handle_etc_invoice(user_query, debug, phone_number, safe_conversation_id, include_analysis, include_chart, timeout, is_monitor, params=params)
        except Exception as e:
            # 如果参数提取失败，仍然调用ETC发票服务，但不传递params参数，让服务内部自动提取
            logging.warning(f"[请求路由] etc_invoice类别参数提取失败，将使用自动参数提取: {str(e)}")
            result = await _handle_etc_invoice(user_query, debug, phone_number, safe_conversation_id, include_analysis, include_chart, timeout, is_monitor)
    elif intent == "etc_sett":
        # 处理ETC结算数据查询
        # 尝试提取参数，如果成功则传递给ETC结算服务
        try:
            params = await convert_params(user_query, "etc_sett", phone_number, history)
            result = await _handle_etc_sett(user_query, debug, phone_number, conversation_id, include_analysis, include_chart, timeout, is_monitor, params=params)
        except Exception as e:
            # 如果参数提取失败，仍然调用ETC结算服务，但不传递params参数，让服务内部自动提取
            logging.warning(f"[请求路由] etc_sett类别参数提取失败，将使用自动参数提取: {str(e)}")
            result = await _handle_etc_sett(user_query, debug, phone_number, conversation_id, include_analysis, include_chart, timeout, is_monitor)
    else:  # chat
        # 对于未识别的意图，默认使用dify服务处理
        result = await _handle_dify(user_query, debug, phone_number, conversation_id, include_analysis, include_chart, timeout, is_monitor)
    
    # 确保result是字典类型
    if not isinstance(result, dict):
        logging.error(f"[步骤0-请求路由] 意外的结果类型: {type(result)}")
        result = {"error": "内部服务器错误", "type": "error"}
    
    end_time = time.time()
    logging.info(f"[步骤0-请求路由] 请求路由耗时: {end_time - start_time:.2f}秒")
    return result
# 各意图处理函数
async def _handle_invoice(user_query: str, debug: bool, phone_number: str, conversation_id: Optional[str] = None, include_analysis: Optional[bool] = None, include_chart: Optional[bool] = None, timeout: int = 60, is_monitor: bool = False):
    from invoice_service import call_invoice_service
    
    result = await call_invoice_service(user_query, phone_number, conversation_id, debug, include_analysis, include_chart, is_monitor=is_monitor)
    
    return {
        "type": "invoice",
        "result": result["result"],
        "conversation_id": result["conversation_id"],
        "raw_data": None if not debug else {
            "user_query": user_query,
            "conversation_id": result["conversation_id"],
            "phone_number": phone_number
        }
    }

async def _handle_sett(user_query: str, debug: bool, phone_number: str, conversation_id: Optional[str] = None, include_analysis: Optional[bool] = None, include_chart: Optional[bool] = None, timeout: int = 60, is_monitor: bool = False, params: Optional[dict] = None):
    from sett_service import call_sett_service
    
    result = await call_sett_service(user_query, phone_number, conversation_id, debug, include_analysis, include_chart, is_monitor=is_monitor, params=params)
    
    return {
        "type": "sett",
        "result": result["result"],
        "conversation_id": result["conversation_id"],
        "raw_data": None if not debug else {
            "user_query": user_query,
            "conversation_id": result["conversation_id"],
            "phone_number": phone_number
        }
    }

async def _handle_etc_invoice(user_query: str, debug: bool, phone_number: str, conversation_id: Optional[str] = None, include_analysis: Optional[bool] = None, include_chart: Optional[bool] = None, timeout: int = 60, is_monitor: bool = False, params: Optional[dict] = None) -> Dict[str, Any]:
    from etc_invoice_service import call_etc_invoice_service
    
    result = await call_etc_invoice_service(
        user_query=user_query,
        phone_number=phone_number,
        conversation_id=conversation_id,
        debug=debug,
        include_analysis=include_analysis,
        include_chart=include_chart,
        is_monitor=is_monitor,
        params=params
    )
    
    return {
        "type": "etc_invoice",
        "result": result["result"],
        "conversation_id": result["conversation_id"],
        "raw_data": None if not debug else {
            "user_query": user_query,
            "conversation_id": result["conversation_id"],
            "phone_number": phone_number
        }
    }


async def _handle_etc_sett(user_query: str, debug: bool, phone_number: str, conversation_id: Optional[str] = None, include_analysis: Optional[bool] = None, include_chart: Optional[bool] = None, timeout: int = 60, is_monitor: bool = False, params: Optional[dict] = None) -> Dict[str, Any]:
    from etc_sett_service import call_etc_sett_service
    
    result = await call_etc_sett_service(
        user_query=user_query,
        phone_number=phone_number,
        conversation_id=conversation_id,
        debug=debug,
        include_analysis=include_analysis,
        include_chart=include_chart,
        is_monitor=is_monitor,
        params=params
    )
    
    return {
        "type": "etc_sett",
        "result": result["result"],
        "conversation_id": result["conversation_id"],
        "raw_data": None if not debug else {
            "user_query": user_query,
            "conversation_id": result["conversation_id"],
            "phone_number": phone_number
        }
    }

async def _handle_dify(user_query: str, debug: bool, conversation_id: Optional[str] = None, timeout: int = 60, is_monitor: bool = False):
    from dify_service import call_dify_workflow
    
    # 处理对话ID
    if conversation_id is None or conversation_id.strip() == "":
        # 创建新对话
        conversation_id = chat_history_manager.create_new_conversation()
        logging.info(f"[Dify对话] 创建新对话: {conversation_id}")
    else:
        # 验证对话ID是否存在
        if not chat_history_manager.get_history(conversation_id):
            # 如果不存在，创建新对话
            conversation_id = chat_history_manager.create_new_conversation()
            logging.info(f"[Dify对话] 对话ID不存在，创建新对话: {conversation_id}")
        else:
            logging.info(f"[Dify对话] 使用现有对话: {conversation_id}")
    
    # 获取对话历史
    history = chat_history_manager.get_history(conversation_id)
    logging.info(f"[Dify对话] 历史消息数量: {len(history)}")
    
    # 使用固定的用户ID
    user_id = "default_user"  
    
    # 将历史消息拼接到用户查询中，形成完整的上下文，限制历史记录数量
    full_query = user_query
    if history:
        # 限制历史记录数量，最多保留最近20条对话（10轮）
        recent_history = history[-20:] if len(history) >= 20 else history
        
        # 特别处理：如果用户查询是"分析一下"这类模糊查询，尝试从历史中找到单位名称
        analysis_keywords = ["分析一下", "分析", "趋势分析", "可视化", "统计", "汇总"]
        is_analysis_query = any(keyword in user_query for keyword in analysis_keywords)
        
        context = "\n\n以下是之前的对话历史：\n"
        for msg in recent_history:
            role = "用户" if msg["role"] == "user" else "助手"
            # 限制每条消息的长度，避免单条消息过长
            content = msg["content"]
            if len(content) > 2000:  # 限制单条消息长度为2000字符
                content = content[:2000] + "..."
            context += f"{role}: {content}\n"
        
        # 如果是分析类查询，添加额外提示
        if is_analysis_query:
            context += "\n注意：用户当前请求是分析类查询，请参考上述对话历史中的单位信息进行回复。\n"
        
        full_query = context + f"\n当前用户问题: {user_query}"
        logging.info(f"[Dify对话] 拼接历史消息到用户查询，历史消息数量: {len(recent_history)}")
    
    # 调用Dify API，不传递conversation_id，完全通过拼接的查询内容管理多轮对话
    logging.info(f"[Dify对话] 调用Dify API，full_query: {full_query[:200]}..., user_id: {user_id}")
    result = await call_dify_workflow(full_query, user_id, None, {}, is_monitor=is_monitor)
    logging.info(f"[Dify对话] Dify API返回结果: {result}")
    
    # 保存对话记录（当前用户查询和模型响应）
    chat_history_manager.add_message(conversation_id, "user", user_query)
    chat_history_manager.add_message(conversation_id, "assistant", result.get("answer", ""))
    
    # 确保只返回我们自己创建的对话ID
    return {
        "type": "dify",
        "result": {
            "answer": result.get("answer", ""),
            "conversation_id": conversation_id  # 使用我们自己管理的对话ID
        },
        "conversation_id": conversation_id,
        "raw_data": None if not debug else {
            "user_query": user_query,
            "conversation_id": conversation_id,
            "user_id": user_id
        }
    }

# 提取清算单位关键词
async def extract_merchant_keyword_from_history(history: List[Dict[str, Any]], phone_number: str) -> Optional[str]:
    """
    从历史对话中提取商户关键词
    
    Args:
        history: 对话历史
        phone_number: 用户手机号
        
    Returns:
        提取到的商户关键词，如果没有提取到则返回None
    """
    try:
        # 获取用户可查询商户列表
        available_merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
        if not available_merchants:
            return None
            
        # 提取商户名称列表
        merchant_names = []
        for merchant in available_merchants:
            if isinstance(merchant, dict):
                merchant_name = merchant.get('merName')
                if merchant_name:
                    merchant_names.append(merchant_name)
        
        if not merchant_names:
            return None
            
        # 构建提示，从历史对话中提取商户关键词
        history_text = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history[-5:]])  # 只使用最近5条历史
        
        prompt = f"""
从以下对话历史中提取用户提到的商户名称。如果用户提到了以下商户列表中的某个商户，请返回该商户的完整名称。

商户列表:
{chr(10).join([f"- {name}" for name in merchant_names])}

对话历史:
{history_text}

请只返回商户名称，如果没有提到任何商户，请返回"无"。
"""
        
        # 调用LLM提取商户关键词
        response = await call_llm_api(prompt)
        merchant_keyword = response.strip()
        
        # 验证提取的关键词是否在商户列表中
        if merchant_keyword and merchant_keyword != "无" and merchant_keyword in merchant_names:
            logging.info(f"[商户提取] 从历史对话中提取到商户: {merchant_keyword}")
            return merchant_keyword
            
        return None
    except Exception as e:
        logging.error(f"[商户提取] 从历史对话中提取商户关键词失败: {str(e)}")
        return None

async def extract_settlement_keyword(user_query: str, history: Optional[List[Dict[str, Any]]] = None, phone_number: str = None) -> str:
    # 第一步：优先从当前查询中直接提取单位名称
    if phone_number:
        try:
            # 获取用户的可查询单位列表
            available_units = await get_user_available_units(phone_number, return_full_info=True)
            
            # 处理查询文本，移除常见后缀
            processed_query = process_text_suffixes(user_query)
            
            # 增强的直接匹配逻辑：检查当前查询中是否明确包含可查询单位名称
            for unit in available_units:
                if isinstance(unit, dict):
                    unit_full_name = unit.get("unitFullName")
                    if unit_full_name and isinstance(unit_full_name, str):
                        # 处理单位名称，移除常见后缀
                        unit_name = process_text_suffixes(unit_full_name.strip())
                        
                        # 1. 直接完整匹配：检查处理后的单位名称是否在查询中
                        if unit_name in processed_query:
                            logging.info(f"[单位匹配] 从当前查询中直接匹配到单位: {unit_name}")
                            return unit_name
                        
                        # 2. 原始名称匹配：检查原始单位名称是否在查询中
                        if unit_full_name.strip() in user_query:
                            logging.info(f"[单位匹配] 从当前查询中匹配到原始单位名称: {unit_full_name}")
                            return unit_full_name.strip()
                        
                        # 3. 智能部分匹配：检查单位名称的核心部分是否在查询中
                        if len(unit_name) > 3:
                            # 提取单位名称的核心部分（例如"上海磁悬浮交通发展有限公司" -> "磁悬浮"）
                            core_parts = []
                            for part in unit_name.split():
                                if len(part) >= 2 and part not in ["上海", "北京", "广州", "深圳", "交通", "发展", "有限", "公司", "集团", "中心", "分公司"]:
                                    core_parts.append(part)
                            
                            # 检查核心部分是否在查询中
                            for core_part in core_parts:
                                if len(core_part) >= 2 and core_part in processed_query:
                                    logging.info(f"[单位匹配] 从当前查询中匹配到单位核心部分: {core_part} -> {unit_name}")
                                    return unit_name
        except Exception as e:
            # 如果获取可查询单位列表失败，记录错误但继续执行原有逻辑
            logging.error(f"[单位匹配] 获取可查询单位列表失败: {str(e)}")
    
    # 第二步：如果当前查询中没有明确单位名称，再考虑历史对话
    history_context = ""
    if history:
        # 增加历史记录数量，取最近10条历史对话，增加上下文
        recent_history = history[-10:] if len(history) >= 10 else history
        
        # 关键改进：检查当前查询是否明确包含新的单位名称
        # 如果当前查询中明确提到了新的单位名称，应该优先使用当前查询中的单位
        
        # 检查当前查询中是否包含明确的单位名称关键词
        unit_keywords_in_current_query = extract_unit_keywords_from_query(user_query)
        if unit_keywords_in_current_query:
            # 如果当前查询中有单位关键词，检查是否与历史对话中的单位不同
            try:
                available_units = await get_user_available_units(phone_number, return_full_info=True)
                current_matched_unit = match_unit_by_keywords(unit_keywords_in_current_query, available_units)
                
                if current_matched_unit:
                    # 检查历史对话中最近提到的单位名称 - 使用更简单的方法
                    historical_unit = ""
                    # 从历史对话中查找最近提到的单位名称
                    for msg in reversed(recent_history):
                        if msg.get('role') == 'assistant':
                            content = msg.get('content', '')
                            # 在系统回复中查找单位名称
                            for unit in available_units:
                                if isinstance(unit, dict):
                                    unit_name = unit.get("unitFullName", "")
                                    if unit_name and unit_name in content:
                                        historical_unit = unit_name
                                        break
                            if historical_unit:
                                break
                    
                    # 如果当前查询匹配到的单位与历史对话中的单位不同，优先使用当前查询的单位
                    if historical_unit and current_matched_unit != historical_unit:
                        logging.info(f"[多轮对话] 检测到新单位名称，优先使用当前查询的单位: {current_matched_unit} (历史单位: {historical_unit})")
                        return current_matched_unit
                    # 如果历史对话中没有单位信息，直接使用当前查询的单位
                    elif not historical_unit:
                        logging.info(f"[多轮对话] 历史对话中没有单位信息，使用当前查询的单位: {current_matched_unit}")
                        return current_matched_unit
                    # 如果当前查询和历史对话的单位相同，继续后续逻辑
                    else:
                        logging.info(f"[多轮对话] 当前查询与历史对话单位相同: {current_matched_unit}")
            except Exception as e:
                logging.error(f"[多轮对话] 检查新单位名称失败: {str(e)}")
        
        history_context = "\n以下是最近的对话历史，用于更好地理解当前问题的上下文：\n"
        
        # 特别关注系统回复（assistant角色）中的单位信息
        assistant_messages = [msg for msg in recent_history if msg.get('role') == 'assistant']
        if assistant_messages:
            history_context += "\n特别注意以下系统回复中可能包含的单位信息：\n"
            for msg in assistant_messages[-3:]:  # 只取最近3条系统回复
                history_context += f"系统回复: {msg['content']}\n"
        
        # 添加所有历史对话
        history_context += "\n完整对话历史：\n"
        for msg in recent_history:
            history_context += f"{msg['role']}: {msg['content']}\n"
    
    # 改进的分析类查询处理：当用户查询中包含分析类词汇时，先检查当前查询是否有新单位
    # 修复：避免普通发票查询被错误分类为分析类查询
    analysis_keywords = ["分析一下", "分析", "趋势分析", "可视化", "统计", "汇总", "以上", "这些", "对数据进行", "数据", "图表"]
    
    # 检查是否为分析类查询 - 修复：仅当包含真正的分析类关键词时才视为分析类查询
    # 排除"发票"、"结算"、"商户"等业务关键词，避免普通业务查询被错误分类
    is_analysis_query = any(keyword in user_query for keyword in analysis_keywords)
    
    # 检查当前查询中是否明确包含单位名称
    # 增强判断：不仅检查标准单位后缀，还要检查是否包含可查询单位的名称
    has_explicit_unit = any(keyword in user_query for keyword in ['公司', '集团', '中心', '分公司', 'merId', 'merName'])
    
    # 如果当前查询中包含明确的单位名称关键词，也视为有明确单位
    if not has_explicit_unit and phone_number:
        try:
            available_units = await get_user_available_units(phone_number, return_full_info=True)
            for unit in available_units:
                if isinstance(unit, dict):
                    unit_full_name = unit.get("unitFullName", "")
                    if unit_full_name and unit_full_name in user_query:
                        has_explicit_unit = True
                        logging.info(f"[单位检测] 检测到明确单位全名: {unit_full_name}")
                        break
                    
                    # 检查单位名称的核心部分是否在查询中
                    unit_name = process_text_suffixes(unit_full_name.strip())
                    if unit_name and unit_name in process_text_suffixes(user_query):
                        has_explicit_unit = True
                        logging.info(f"[单位检测] 检测到单位核心名称: {unit_name}")
                        break
                    
                    # 增强检测：检查单位名称的简化版本（去掉地区前缀等）
                    simplified_name = unit_full_name.replace("上海", "").replace("公司", "").replace("出租汽车", "").strip()
                    if simplified_name and simplified_name in user_query:
                        has_explicit_unit = True
                        logging.info(f"[单位检测] 检测到简化单位名称: {simplified_name}")
                        break
        except Exception as e:
            logging.error(f"[单位检测] 检查当前查询单位匹配失败: {str(e)}")
    
    # 额外检查：如果查询中包含2个以上中文字符且不是常见业务词汇，也视为可能有明确单位
    if not has_explicit_unit:
        import re
        chinese_words = re.findall(r'[\u4e00-\u9fff]{2,}', user_query)
        business_keywords = ['发票', '结算', '轧差', '分析', '查询', '数据', '趋势', '可视化', '统计', '报告', 'ETC']
        
        # 检查是否有非业务相关的中文词汇（可能是单位名称）
        potential_unit_words = [word for word in chinese_words if word not in business_keywords]
        if len(potential_unit_words) >= 2:  # 如果有2个或以上非业务词汇，可能包含单位名称
            has_explicit_unit = True
            logging.info(f"[单位检测] 检测到潜在单位词汇: {potential_unit_words}")
    
    # 增强的新单位检测：检查当前查询中是否有新单位指示词
    new_unit_keywords = ["新", "另一个", "其他", "别的", "不同", "更换", "切换", "换到", "改为", "改成", "修改为", "更改为"]
    has_new_unit_indicator = any(keyword in user_query for keyword in new_unit_keywords)
    
    # 如果当前查询中有新单位指示词，优先使用当前查询中的单位
    if has_new_unit_indicator:
        # 尝试从当前查询中提取单位关键词
        extracted_keywords = extract_unit_keywords_from_query(user_query)
        if extracted_keywords:
            # 使用关键词匹配最可能的单位
            if phone_number:
                try:
                    available_units = await get_user_available_units(phone_number, return_full_info=True)
                    matched_unit = match_unit_by_keywords(extracted_keywords, available_units)
                    if matched_unit:
                        logging.info(f"[新单位检测] 检测到新单位指示词，使用当前查询匹配的单位: {matched_unit}")
                        return matched_unit
                except Exception as e:
                    logging.error(f"[新单位检测] 匹配单位失败: {str(e)}")
    
    if is_analysis_query:
        # 对于分析类查询，始终优先检查当前查询中的单位信息
        
        # 1. 首先检查当前查询中是否有可匹配的单位关键词
        current_matched_unit = None
        if phone_number:
            try:
                available_units = await get_user_available_units(phone_number, return_full_info=True)
                
                # 检查当前查询中是否包含可查询单位的名称
                for unit in available_units:
                    if isinstance(unit, dict):
                        unit_full_name = unit.get("unitFullName", "")
                        if unit_full_name and unit_full_name in user_query:
                            logging.info(f"[分析类查询] 当前查询中直接匹配到单位: {unit_full_name}")
                            current_matched_unit = unit_full_name
                            break
                        
                        # 检查单位名称的核心部分是否在查询中
                        unit_name = process_text_suffixes(unit_full_name.strip())
                        if unit_name and unit_name in process_text_suffixes(user_query):
                            logging.info(f"[分析类查询] 当前查询中匹配到单位核心名称: {unit_name}")
                            current_matched_unit = unit_name
                            break
            except Exception as e:
                logging.error(f"[分析类查询] 检查当前查询单位匹配失败: {str(e)}")
        
        # 修复：如果当前查询中匹配到了单位，直接返回，不进入历史对话提取逻辑
        if current_matched_unit:
            logging.info(f"[分析类查询] 当前查询已匹配到单位，直接返回: {current_matched_unit}")
            return current_matched_unit
        
        # 2. 只有当前查询中没有明确单位时，才考虑从历史对话中提取
        if history:
            keyword = await _extract_unit_from_history(history_context, is_etc=True)
            if keyword:
                logging.info(f"[分析类查询] 当前查询无匹配单位，从历史对话中提取单位/商户: {keyword}")
                return keyword
            
            # 如果从历史对话中提取失败，尝试获取用户的第一个可查询单位
            try:
                available_units = await get_user_available_units(phone_number, return_full_info=True)
                if available_units and len(available_units) > 0:
                    # 获取第一个单位的名称
                    first_unit = available_units[0]
                    if isinstance(first_unit, dict):
                        unit_name = first_unit.get("unitFullName", first_unit.get("unitName", "未知单位"))
                    else:
                        unit_name = str(first_unit)
                    
                    logging.info(f"[分析类查询] 使用第一个可查询单位作为备选: {unit_name}")
                    return unit_name
            except Exception as e:
                logging.error(f"[分析类查询] 获取可查询单位列表失败: {str(e)}")
                
                # 对于ETC相关查询，尝试获取商户列表
                try:
                    merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
                    if merchants and len(merchants) > 0:
                        first_merchant = merchants[0]
                        merchant_name = first_merchant.get("merName", "未知商户")
                        logging.info(f"[分析类查询] 使用第一个可用商户: {merchant_name}")
                        return merchant_name
                except Exception as merchant_e:
                    logging.error(f"[分析类查询] 获取商户列表失败: {str(merchant_e)}")
            
            # 如果两边都没找到匹配上的，则默认为清算单位的第一个
            try:
                available_units = await get_user_available_units(phone_number, return_full_info=True)
                if available_units and len(available_units) > 0:
                    # 获取第一个单位的名称
                    first_unit = available_units[0]
                    if isinstance(first_unit, dict):
                        unit_name = first_unit.get("unitFullName", first_unit.get("unitName", "未知单位"))
                    else:
                        unit_name = str(first_unit)
                    
                    logging.info(f"[分析类查询] 两边都没匹配上，使用第一个可查询单位作为默认: {unit_name}")
                    return unit_name
            except Exception as e:
                logging.error(f"[分析类查询] 获取可查询单位列表失败: {str(e)}")
                
                # 最后尝试获取商户列表作为备选
                try:
                    merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
                    if merchants and len(merchants) > 0:
                        first_merchant = merchants[0]
                        merchant_name = first_merchant.get("merName", "未知商户")
                        logging.info(f"[分析类查询] 两边都没匹配上，使用第一个可用商户作为默认: {merchant_name}")
                        return merchant_name
                except Exception as merchant_e:
                    logging.error(f"[分析类查询] 获取商户列表失败: {str(merchant_e)}")
    
    # 改进提示词，明确优先级：当前查询中的单位名称始终优先
    prompt = f"""从用户需求和对话历史中提取清算单位核心关键词，按以下优先级：
1. 优先从当前用户需求中提取清算单位名称
2. 如果当前需求中实在没有提取到单位名称，则从对话历史中查找最近提到的清算单位
3. 忽略'公司'、'有限公司'、'分公司'等后缀，仅返回核心名称
4. 当前查询中的单位名称始终优先于历史对话中的单位信息
5. 如果用户查询中不包含"公司"、"集团"等关键词，但提到了"发票"、"结算单"、"轧差表"、"ETC"等业务相关词汇或分析类词汇，请尝试推断用户可能想查询的单位
6. 对于ETC相关查询，请提取商户名称而非清算单位名称
7. 仅返回清算单位或商户核心关键词，不要解释

{history_context}
当前用户需求：{user_query}

请返回清算单位或商户核心关键词："""
    
    keyword = (await LLMService.call_qwen_model(prompt, timeout=30)).strip()
    
    # 清理可能的无关词汇
    invalid_keywords = ['趋势分析', '可视化', '分析', '图表', '数据', '统计', '报告', '轧差表', '结算单', '以上', '这些', 'ETC', '发票', '结算轧差表', '结算', '清算']
    if keyword.lower() in [k.lower() for k in invalid_keywords]:
        # 如果提取到的是分析相关词汇或'轧差表'，尝试从历史中重新提取
        if history:
            keyword = await _extract_unit_from_history(history_context, is_etc=('ETC' in user_query))
    
    # 如果仍然没有提取到有效关键词，尝试使用更智能的推断
    if not keyword or keyword.lower() in [k.lower() for k in invalid_keywords]:
        # 检查是否包含业务相关词汇但没有明确单位
        business_keywords = ['发票', '结算单', '轧差表', 'ETC', '消费', '账单', '结算轧差表', '结算', '清算']
        if any(biz_keyword in user_query for biz_keyword in business_keywords) and history:
            # 使用更智能的提示词尝试推断单位
            inference_prompt = f"""用户提到了以下业务相关内容，但没有明确指出单位名称。请根据对话历史和上下文，推断用户可能想查询的单位名称。
对于ETC相关查询，请推断商户名称。

{history_context}
当前用户需求：{user_query}

请返回最可能的清算单位或商户核心关键词（如果无法确定，请返回历史对话中最近提到的单位）："""
            keyword = (await LLMService.call_qwen_model(inference_prompt, timeout=30)).strip()
            
            # 再次验证提取的关键词
            if keyword and keyword.lower() not in [k.lower() for k in invalid_keywords]:
                return keyword
    
    # 如果仍然没有提取到有效关键词，对于ETC相关查询尝试获取商户列表
    if not keyword or keyword.lower() in [k.lower() for k in invalid_keywords]:
        if 'ETC' in user_query:
            try:
                merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
                if merchants and len(merchants) > 0:
                    first_merchant = merchants[0]
                    merchant_name = first_merchant.get("merName", "未知商户")
                    logging.info(f"[商户匹配] 使用第一个可用商户: {merchant_name}")
                    return merchant_name
            except Exception as e:
                logging.error(f"[商户匹配] 获取商户列表失败: {str(e)}")
    
    # 如果仍然没有提取到有效关键词，将其视为模糊查询
    if not keyword or keyword.lower() in [k.lower() for k in invalid_keywords]:
        logging.info(f"[关键词提取] 无法提取到有效关键词，视为模糊查询")
        # 返回一个通用的模糊关键词，让模糊查询逻辑处理
        return "查询"
    
    return keyword

# 辅助函数：从查询中提取单位关键词
def extract_unit_keywords_from_query(user_query: str) -> list:
    """
    从用户查询中提取可能的单位名称关键词
    
    Args:
        user_query: 用户查询文本
        
    Returns:
        list: 提取到的关键词列表
    """
    import re
    
    # 常见单位名称关键词模式
    unit_patterns = [
        r'[\u4e00-\u9fff]+(?:公司|集团|中心|分公司|有限公司|有限责任公司)',  # 中文单位名称
        r'[A-Za-z]+(?:公司|集团|中心|分公司|有限公司|有限责任公司)',  # 英文+中文后缀
        r'[A-Za-z\s]+(?:Co\.|Inc\.|Ltd\.|Corp\.)',  # 英文公司名称
    ]
    
    keywords = []
    for pattern in unit_patterns:
        matches = re.findall(pattern, user_query)
        keywords.extend(matches)
    
    # 如果未找到标准模式，尝试提取包含2个以上中文字符的短语
    if not keywords:
        chinese_phrases = re.findall(r'[\u4e00-\u9fff]{2,}', user_query)
        # 过滤掉常见非单位词汇
        common_non_unit_words = ['发票', '结算', '轧差', '分析', '查询', '数据', '趋势', '可视化', '统计', '报告']
        keywords = [phrase for phrase in chinese_phrases if phrase not in common_non_unit_words]
    
    return keywords

# 辅助函数：根据关键词匹配单位
def match_unit_by_keywords(keywords: list, available_units: list) -> str:
    """
    根据关键词列表匹配最可能的单位
    
    Args:
        keywords: 关键词列表
        available_units: 可查询单位列表
        
    Returns:
        str: 匹配到的单位名称，如果没有匹配则返回空字符串
    """
    if not keywords or not available_units:
        return ""
    
    best_match = ""
    best_score = 0
    
    for unit in available_units:
        if isinstance(unit, dict):
            unit_name = unit.get("unitFullName", "")
            if not unit_name:
                continue
                
            # 计算匹配分数
            score = 0
            for keyword in keywords:
                if keyword in unit_name:
                    # 关键词在单位名称中出现的次数越多，分数越高
                    score += unit_name.count(keyword) * len(keyword)
            
            # 如果单位名称完全包含某个关键词，给予更高分数
            for keyword in keywords:
                if keyword == unit_name:
                    score += 100  # 完全匹配给予最高分数
                elif keyword in unit_name:
                    score += 50   # 部分匹配给予较高分数
            
            if score > best_score:
                best_score = score
                best_match = unit_name
    
    # 只有当匹配分数达到一定阈值时才返回结果
    if best_score >= 10:  # 设置一个合理的阈值
        return best_match
    else:
        return ""

# 辅助函数：从历史对话中提取清算单位或商户
async def _extract_unit_from_history(history_context: str, is_etc: bool = False) -> str:
    """
    从历史对话中提取清算单位名称或商户名称
    
    Args:
        history_context: 历史对话上下文
        is_etc: 是否为ETC相关查询，如果是则需要提取商户名称而非单位名称
        
    Returns:
        str: 提取到的单位/商户名称，如果提取失败则返回空字符串
    """
    entity_type = "商户" if is_etc else "清算单位"
    
    fallback_prompt = f"""从以下对话历史中找到最近提到的{entity_type}名称，忽略后缀，只返回核心名称。
特别注意：
1. 如果用户提到"分析一下"等分析类请求，请找出之前对话中提到的{entity_type}名称
2. 优先查找系统回复（assistant角色）中提到的{entity_type}名称，因为系统回复通常包含更准确的单位信息
3. 如果用户查询中包含"发票"、"轧差表"、"结算单"、"ETC"等词汇但没有明确{entity_type}，请从历史对话中推断
4. 对于ETC相关查询，请查找"merId"、"merName"或商户相关信息
5. 对于清算单位查询，请查找公司、机构或单位相关信息
6. 重点关注最近几轮对话中的{entity_type}信息，尤其是系统明确提到的单位
7. 只返回{entity_type}名称，不要返回其他内容

{history_context}
请返回{entity_type}核心关键词："""
    keyword = (await LLMService.call_qwen_model(fallback_prompt, timeout=30)).strip()
    
    # 验证提取的关键词是否有效
    invalid_keywords = ['趋势分析', '可视化', '分析', '图表', '数据', '统计', '报告', '轧差表', '结算单', '以上', '这些', '分析一下', '对数据进行', '发票', 'ETC', '商户', '结算轧差表', '结算', '清算']
    if keyword and keyword.lower() not in [k.lower() for k in invalid_keywords]:
        return keyword
        
    # 如果第一次尝试失败，使用更具体的提示词再次尝试
    second_prompt = f"""从以下对话历史中查找{entity_type}名称。请仔细阅读对话内容，找出任何提到的公司、机构或{entity_type}名称。
只返回{entity_type}的核心名称，不要包含"公司"、"有限公司"等后缀。

{history_context}
{entity_type}名称："""
    keyword = (await LLMService.call_qwen_model(second_prompt, timeout=30)).strip()
    
    # 再次验证提取的关键词
    if keyword and keyword.lower() not in [k.lower() for k in invalid_keywords]:
        return keyword
        
    # 对于ETC查询，尝试使用正则表达式提取商户ID或名称
    if is_etc:
        import re
        # 尝试提取merId或merName
        mer_id_match = re.search(r'merId["\']?\s*[:=]\s*["\']?([^"\'\s,}]+)', history_context)
        if mer_id_match:
            mer_id = mer_id_match.group(1)
            logging.info(f"[商户匹配] 从历史对话中提取到商户ID: {mer_id}")
            return mer_id
            
        mer_name_match = re.search(r'merName["\']?\s*[:=]\s*["\']([^"\']+)["\']', history_context)
        if mer_name_match:
            mer_name = mer_name_match.group(1)
            logging.info(f"[商户匹配] 从历史对话中提取到商户名称: {mer_name}")
            return mer_name
    
    return ""

async def get_user_available_units(phone_number: str, return_full_info: bool = False) -> list:
    """
    根据手机号查询用户可以查询的所有单位列表
    
    Args:
        phone_number: 用户手机号
        return_full_info: 是否返回完整的单位信息（包括unitId），默认只返回单位名称
        
    Returns:
        list: 单位列表（名称列表或完整信息列表）
        
    Raises:
        HTTPException: 当API请求失败或数据格式错误时
    """
    # 1. 调用API查询单位列表
    try:
        response = await service.call_api("/ai/api/v1/queryUnitList", {"phoneNo": phone_number})
        logging.info("[单位列表API] 请求成功")
    except Exception as e:
        raise HTTPException(status_code=503, detail=f"单位列表API请求失败：{str(e)}")
    
    # 2. 解析API响应（call_api已经返回了解析后的字典）
    result = response
    
    # 3. 类型检查：确保返回的是字典类型
    if not isinstance(result, dict):
        logging.error(f"[单位列表API] 返回类型错误：期望字典，实际是 {type(result)}")
        raise HTTPException(status_code=500, detail="API返回类型错误")
    
    # 4. 检查响应状态
    resp_code = result.get("respCode")
    if not resp_code or resp_code != "00":
        error_msg = result.get("respMsg", "未知错误")
        if not isinstance(error_msg, str):
            error_msg = str(error_msg) if error_msg else "未知错误"
        logging.error(f"[单位列表API] 响应状态异常：respCode={resp_code}, respMsg={error_msg}")
        raise HTTPException(status_code=400, detail=f"查询失败：{error_msg}")
    
    # 5. 提取单位列表
    unit_list = result.get("unitList", [])
    if not isinstance(unit_list, list):
        logging.error(f"[单位列表API] unitList类型错误：期望列表，实际是 {type(unit_list)}")
        raise HTTPException(status_code=500, detail="单位列表数据格式错误")
    
    # 6. 验证单位列表不为空
    if not unit_list:
        logging.warning(f"[单位列表API] 用户无可用清算单位")
        return []
    
    # 7. 根据参数返回不同格式的单位列表
    if return_full_info:
        # 返回完整的单位信息
        return unit_list
    else:
        # 只返回单位名称列表，使用安全提取函数
        available_units = [safe_extract_unit_name(unit) for unit in unit_list]
        return available_units

async def get_unit_id_by_phone_and_keyword(phone_number: str, keyword: str, history: list = None) -> list:
    """
    根据手机号和关键词匹配获取对应的unitId
    优化后的执行顺序：
    1. 获取基础数据
    2. 预筛选（当单位数量较多时）
    3. 尝试多种匹配策略
    4. 按优先级返回结果
    
    Args:
        phone_number: 用户手机号
        keyword: 查询关键词
        history: 历史对话列表，用于提供上下文信息
        
    Returns:
        list: 匹配的单位ID列表
    """
    try:
        # 1. 首先获取用户可查询单位列表（只获取一次）
        unit_list = await get_user_available_units(phone_number, return_full_info=True)
        
        # 2. 处理关键词
        # 先检查是否为停车场查询，避免process_text_suffixes影响判断
        is_parking = is_parking_query(keyword)
        processed_keyword = process_text_suffixes(keyword)
        
        # 3. 当单位数量较多时（>100），进行预筛选以提高匹配效率
        if len(unit_list) > 100:
            logging.info(f"[单位匹配] 单位数量较多({len(unit_list)}个)，进行预筛选")
            unit_list = _pre_filter_units(processed_keyword, unit_list)
            logging.info(f"[单位匹配] 预筛选后单位数量: {len(unit_list)}个")
        
        # 4. 按优先级尝试匹配策略
        # 策略1: LLM智能匹配（最准确）
        logging.info(f"[单位匹配] 开始策略1: LLM智能匹配，关键词: {processed_keyword}")
        
        # 构建历史对话上下文
        history_context = ""
        if history:
            # 增加历史记录数量，取最近10条历史对话，增加上下文
            recent_history = history[-10:] if len(history) >= 10 else history
            history_context = "\n以下是最近的对话历史，用于更好地理解当前问题的上下文：\n"
            
            # 特别关注系统回复（assistant角色）中的单位信息
            assistant_messages = [msg for msg in recent_history if msg.get('role') == 'assistant']
            if assistant_messages:
                history_context += "\n特别注意以下系统回复中可能包含的单位信息：\n"
                for msg in assistant_messages[-3:]:  # 只取最近3条系统回复
                    history_context += f"系统回复: {msg['content']}\n"
            
            # 添加所有历史对话
            history_context += "\n完整对话历史：\n"
            for msg in recent_history:
                history_context += f"{msg['role']}: {msg['content']}\n"
        
        # 对于停车场查询，清除LLM缓存以避免旧缓存导致的错误匹配
        if is_parking:
            logging.info(f"[单位匹配] 检测到停车场查询，清除LLM缓存以避免错误匹配")
            LLMService.clear_cache()
        
        matched_ids = await llm_smart_match(processed_keyword, unit_list, return_multiple=False, history_context=history_context)
        if matched_ids:
            logging.info(f"[单位匹配] 使用LLM智能匹配成功: {matched_ids[0]}")
            return matched_ids
        else:
            logging.info(f"[单位匹配] LLM智能匹配失败，进入下一个策略")
        
        # 策略2: 传统字符串匹配（快速）
        logging.info(f"[单位匹配] 开始策略2: 传统字符串匹配，关键词: {processed_keyword}")
        matched_ids = _string_matching(processed_keyword, unit_list)
        if matched_ids:
            logging.info(f"[单位匹配] 使用字符串匹配成功: {matched_ids[0]}")
            return matched_ids
        else:
            logging.info(f"[单位匹配] 字符串匹配失败，进入下一个策略")
        
        # 策略3: 相似度计算（备选）
        logging.info(f"[单位匹配] 开始策略3: 相似度计算，关键词: {processed_keyword}")
        matched_ids = _similarity_matching(processed_keyword, unit_list)
        if matched_ids:
            logging.info(f"[单位匹配] 使用相似度计算匹配成功: {matched_ids[0]}")
            return matched_ids
        else:
            logging.info(f"[单位匹配] 相似度计算匹配失败，进入下一个策略")
        
        # 策略4: 特殊情况处理（用户只有一个单位）
        logging.info(f"[单位匹配] 开始策略4: 特殊情况处理，关键词: {processed_keyword}")
        matched_ids = _single_unit_case(processed_keyword, unit_list)
        if matched_ids:
            logging.info(f"[单位匹配] 使用特殊情况处理匹配成功: {matched_ids[0]}")
            return matched_ids
        else:
            logging.info(f"[单位匹配] 特殊情况处理失败")
        
        # 所有策略都失败，使用第一个可查询单位
        logging.warning(f"[单位匹配] 所有匹配策略都失败，使用第一个可查询单位")
        if unit_list and len(unit_list) > 0:
            first_unit = unit_list[0]
            if isinstance(first_unit, dict):
                unit_id = first_unit.get("unitId")
                unit_name = first_unit.get("unitFullName", "未知单位")
            else:
                unit_id = first_unit
                unit_name = "未知单位"
            
            logging.info(f"[单位匹配] 使用第一个可查询单位: {unit_name} (ID: {unit_id})")
            return [unit_id]
        else:
            logging.error(f"[单位匹配] 没有可用的单位列表")
            return []
        
    except HTTPException as e:
        raise e

def _string_matching(keyword: str, unit_list: list) -> list:
    """字符串匹配逻辑，简化版本不区分停车场和公司"""
    matched_unit_ids = []
    
    for unit in unit_list:
        if not isinstance(unit, dict):
            continue
            
        unit_full_name = unit.get("unitFullName")
        unit_desc = unit.get("unitDesc")
        
        if not unit_full_name or not isinstance(unit_full_name, str):
            continue
        
        # 先尝试使用停车场处理函数，如果包含停车场后缀
        if is_parking_query(keyword) or is_parking_query(unit_full_name):
            unit_name = process_parking_name(unit_full_name.strip())
            processed_keyword = process_parking_name(keyword)
        else:
            unit_name = process_text_suffixes(unit_full_name.strip())
            processed_keyword = process_text_suffixes(keyword)
            
        unit_desc = str(unit_desc).lower() if unit_desc else ""
        
        # 直接匹配
        if processed_keyword in unit_name or processed_keyword in unit_desc:
            unit_id = unit.get("unitId")
            if unit_id:
                matched_unit_ids.append(unit_id)
        # 如果关键词很短（1-2个字符），尝试更宽松的匹配
        elif len(processed_keyword) <= 2 and any(char in unit_name for char in processed_keyword):
            unit_id = unit.get("unitId")
            if unit_id:
                matched_unit_ids.append(unit_id)
    
    # 如果有多个匹配，只返回第一个
    if matched_unit_ids:
        return [matched_unit_ids[0]]
    
    return []

def _similarity_matching(keyword: str, unit_list: list, threshold: float = 0.3) -> list:
    """相似度匹配逻辑"""
    similarity_scores = []
    for unit in unit_list:
        if not isinstance(unit, dict):
            continue
            
        unit_id = unit.get("unitId")
        if not unit_id:
            continue
            
        unit_full_name = unit.get("unitFullName")
        unit_desc = unit.get("unitDesc")
        
        if not unit_full_name or not isinstance(unit_full_name, str):
            continue
            
        unit_name = process_text_suffixes(unit_full_name.strip())
        unit_desc = str(unit_desc).lower() if unit_desc else ""
        
        # 使用新的地理位置相似度计算方法
        name_similarity = calculate_location_similarity(keyword, unit_name)
        desc_similarity = calculate_location_similarity(keyword, unit_desc)
        max_similarity = max(name_similarity, desc_similarity)
        
        if max_similarity >= threshold:
            similarity_scores.append({
                "unit_id": unit_id,
                "similarity": max_similarity
            })
    
    if similarity_scores:
        similarity_scores.sort(key=lambda x: x["similarity"], reverse=True)
        best_match = similarity_scores[0]
        logging.info(f"[单位匹配] 相似度计算最佳匹配: {best_match['unit_id']}, 相似度: {best_match['similarity']*100:.2f}%")
        return [best_match["unit_id"]]
    
    return []

def _pre_filter_units(keyword: str, unit_list: list) -> list:
    """
    当单位数量较多时，进行预筛选以提高匹配效率
    
    Args:
        keyword: 处理后的关键词
        unit_list: 完整的单位列表
        
    Returns:
        预筛选后的单位列表
    """
    if not keyword or not unit_list:
        return unit_list
    
    # 计算每个单位与关键词的相似度
    similarity_scores = []
    for unit in unit_list:
        if not isinstance(unit, dict):
            continue
            
        unit_id = unit.get("unitId")
        if not unit_id:
            continue
            
        unit_full_name = unit.get("unitFullName")
        unit_desc = unit.get("unitDesc")
        
        if not unit_full_name or not isinstance(unit_full_name, str):
            continue
            
        # 先尝试使用停车场处理函数，如果包含停车场后缀
        if is_parking_query(keyword) or is_parking_query(unit_full_name):
            unit_name = process_parking_name(unit_full_name.strip())
            processed_keyword = process_parking_name(keyword)
        else:
            unit_name = process_text_suffixes(unit_full_name.strip())
            processed_keyword = process_text_suffixes(keyword)
            
        unit_desc = str(unit_desc).lower() if unit_desc else ""
        
        # 使用新的地理位置相似度计算方法
        location_similarity = calculate_location_similarity(processed_keyword, unit_name)
        
        # 计算描述相似度（如果有的话）
        desc_similarity = 0
        if unit_desc:
            desc_similarity = calculate_location_similarity(processed_keyword, unit_desc)
        
        # 子串匹配得分
        substring_score = 0
        keyword_lower = processed_keyword.lower()
        if keyword_lower in unit_name.lower():
            substring_score = 0.7  # 降低子串匹配得分，避免"P+R"这样的通用词影响过大
        elif any(word in unit_name.lower() for word in keyword_lower.split() if len(word) > 1):
            substring_score = 0.5  # 降低部分词匹配得分
            
        # 综合相似度 - 不区分停车场和公司类型
        max_similarity = max(location_similarity, desc_similarity, substring_score)
        
        similarity_scores.append({
            "unit": unit,
            "similarity": max_similarity,
            "location_similarity": location_similarity,
            "desc_similarity": desc_similarity,
            "substring_score": substring_score
        })
    
    # 按相似度排序并选择前50%或前100个单位（取较小值）
    similarity_scores.sort(key=lambda x: x["similarity"], reverse=True)
    
    # 确保至少保留50个单位，最多保留200个
    min_units = min(50, len(unit_list))
    max_units = min(200, len(unit_list))
    
    # 根据相似度分布动态调整保留数量
    if similarity_scores:
        # 计算相似度阈值
        high_similarity_count = sum(1 for item in similarity_scores if item["similarity"] > 0.5)
        
        if high_similarity_count >= min_units:
            # 如果高相似度单位足够，只保留高相似度单位
            filtered_units = [item["unit"] for item in similarity_scores if item["similarity"] > 0.5]
        else:
            # 否则保留前max_units个单位
            filtered_units = [item["unit"] for item in similarity_scores[:max_units]]
            
        # 确保至少保留min_units个单位
        if len(filtered_units) < min_units:
            filtered_units = [item["unit"] for item in similarity_scores[:min_units]]
            
        return filtered_units
    
    return unit_list

def _single_unit_case(keyword: str, unit_list: list) -> list:
    """处理用户只有一个单位的特殊情况"""
    if len(unit_list) == 1 and keyword:
        unit = unit_list[0]
        if isinstance(unit, dict):
            unit_full_name = unit.get("unitFullName")
            unit_desc = unit.get("unitDesc")
            
            if unit_full_name and isinstance(unit_full_name, str):
                unit_name = unit_full_name.lower().strip()
                unit_desc = str(unit_desc).lower() if unit_desc else ""
                unit_name = process_text_suffixes(unit_name)
                
                if (keyword in unit_name or 
                    keyword in unit_desc or
                    any(char in unit_name for char in keyword if len(keyword) <= 2)):
                    unit_id = unit.get("unitId")
                    if unit_id:
                        logging.info(f"[单位匹配] 用户只有一个可查询单位且关键词相关，直接返回: {unit_id}")
                        return [unit_id]
                else:
                    logging.info(f"[单位匹配] 用户只有一个可查询单位但关键词不相关，不自动匹配")
    
    return []

# 智能判断函数
def should_include_analysis(user_query: str, analysis_type: str) -> bool:
    """智能判断是否需要数据分析"""
    # 基础分析关键词
    base_keywords = ["分析", "趋势", "统计", "汇总", "对比", "报告"]
    
    # 根据分析类型添加特定关键词
    if analysis_type == "sett":
        base_keywords.append("消费情况")
    elif analysis_type == "etc_invoice":
        base_keywords.extend(["ETC发票分析", "开票情况", "ETC消费"])
    elif analysis_type == "etc_sett":
        base_keywords.extend(["ETC交易汇总", "ETC交易分析", "ETC汇总", "ETC轧差", "停车场轧差", "ETC结算", "停车场结算"])
    
    return any(keyword in user_query for keyword in base_keywords)

def should_include_chart(user_query: str, analysis_type: str) -> bool:
    """
    智能判断是否需要趋势图 - 基于关键词自动判断
    
    Args:
        user_query: 用户查询内容
        analysis_type: 分析类型 ("invoice", "sett", "etc_invoice")
        
    Returns:
        bool: 是否需要生成图表
    """
    # 基础图表关键词，与分析判断逻辑保持一致
    chart_keywords = ["分析", "趋势", "统计", "汇总", "对比", "报告", "图表", "可视化", "趋势图", "画图"]
    
    # 根据分析类型添加特定关键词
    if analysis_type == "sett":
        chart_keywords.append("消费情况")
    elif analysis_type == "etc_invoice":
        chart_keywords.extend(["ETC发票分析", "开票情况", "ETC消费"])
    elif analysis_type == "etc_sett":
        chart_keywords.extend(["ETC交易汇总", "ETC交易分析", "ETC汇总", "ETC轧差", "停车场轧差", "ETC结算", "停车场结算"])
    
    # 检查是否包含图表相关关键词
    return any(keyword in user_query for keyword in chart_keywords)

# 图表生成工具
class TrendAnalyzer:
    @staticmethod
    def generate_trend_chart(data_list: list, date_field: str, value_field: str, title: str, x_label: str, y_label: str, analysis_type: str, value_converter=lambda x: float(x), group_field: str = None) -> dict:
        plt.close()

        # 确保有有效数据
        if not data_list:
            raise ValueError("数据列表为空，无法生成图表")
        
        # 如果指定了分组字段，生成多商户对比趋势图
        if group_field and any(item.get(group_field) for item in data_list):
            return generate_multi_merchant_trend_chart(data_list, date_field, value_field, title, x_label, y_label, analysis_type, value_converter, group_field)
        
        # 单商户趋势图逻辑
        dates = [item.get(date_field) for item in data_list if item.get(date_field)]
        values = [value_converter(item.get(value_field, '0')) for item in data_list if item.get(value_field)]
        
        # 确保有有效数据点
        if not dates or not values:
            raise ValueError("没有有效的日期或数值数据，无法生成图表")

        # 确保保存目录存在
        save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'charts')
        os.makedirs(save_dir, exist_ok=True)
        
        # 生成唯一文件名（基于时间戳）
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f'{title.replace(" ", "_")}_{timestamp}.png'
        file_path = os.path.join(save_dir, filename)
        
        # 绘制图片并保存
        plt.figure(figsize=(10, 6))
        
        # 尝试多种方式设置中文字体
        font_set_success = False
        
        # 方法1: 使用fontManager中已加载的字体
        # 首先检查是否已经加载了中文字体
        try:
            # 获取已加载的字体列表
            font_names = [f.name for f in fontManager.ttflist]
            chinese_fonts = ["思源黑体", "Source Han Sans", "Noto Sans CJK", "Microsoft YaHei", "SimHei", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei"]
            
            for font_name in chinese_fonts:
                # 检查是否有匹配的字体
                matched_fonts = [f for f in font_names if font_name in f]
                if matched_fonts:
                    plt.rcParams['font.family'] = matched_fonts[0]
                    print(f"使用已加载的字体: {matched_fonts[0]}")
                    font_set_success = True
                    break
        except Exception as e:
            print(f"检查已加载字体失败: {str(e)}")
        
        # 方法2: 如果方法1失败，尝试直接使用字体文件路径设置字体
        if not font_set_success:
            # 尝试多个可能的字体路径
            possible_font_paths = [
                # 基于当前文件位置的相对路径
                os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fonts', '思源黑体CN-Heavy.otf'),
                # 基于当前工作目录的相对路径
                os.path.join(os.getcwd(), 'fonts', '思源黑体CN-Heavy.otf'),
                # Linux环境下的可能路径
                '/home/jiuyv/ccagent/fonts/思源黑体CN-Heavy.otf',
                '/home/jiuyy/ccagent/fonts/思源黑体CN-Heavy.otf',
                '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
                '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
                '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
                # Windows环境下的系统字体路径
                'C:/Windows/Fonts/simhei.ttf',
                'C:/Windows/Fonts/msyh.ttf',
                'C:/Windows/Fonts/msyh.ttc',
            ]
            
            font_path = None
            for path in possible_font_paths:
                if os.path.exists(path):
                    font_path = path
                    print(f"找到字体文件: {font_path}")
                    break
            
            if font_path:
                try:
                    font_prop = FontProperties(fname=font_path)
                    plt.rcParams['font.family'] = font_prop.get_name()
                    print(f"成功加载字体: {font_prop.get_name()}")
                    font_set_success = True
                except Exception as e:
                    print(f"加载字体失败: {str(e)}")
            else:
                print("未找到可用的字体文件")
        
        # 方法3: 如果前两种方法都失败，尝试使用fontManager添加字体
        if not font_set_success and font_path:
            try:
                # 清除字体缓存
                fontManager._rebuild()
                fontManager.addfont(font_path)
                font_prop = FontProperties(fname=font_path)
                plt.rcParams['font.family'] = font_prop.get_name()
                print(f"通过fontManager成功加载字体: {font_prop.get_name()}")
                font_set_success = True
            except Exception as e:
                print(f"通过fontManager加载字体失败: {str(e)}")
        elif not font_set_success:
            print("没有可用的字体文件，跳过fontManager加载步骤")
        
        # 方法4: 如果所有方法都失败，直接使用英文标签
        if not font_set_success:
            print("中文字体加载失败，直接使用英文标签")
            # 将中文标签替换为英文
            if "金额" in y_label:
                y_label = y_label.replace("金额", "Amount")
            if "日期" in x_label:
                x_label = x_label.replace("日期", "Date")
            if "趋势" in title:
                title = title.replace("趋势", "Trend")
            if "分析" in title:
                title = title.replace("分析", "Analysis")
            if "图表" in title:
                title = title.replace("图表", "Chart")
            if "统计" in title:
                title = title.replace("统计", "Statistics")
            if "报告" in title:
                title = title.replace("报告", "Report")
            
            # 使用默认字体
            plt.rcParams['font.family'] = 'DejaVu Sans'
            font_set_success = True
        
        plt.rcParams['axes.unicode_minus'] = False
        
        plt.plot(dates, values, marker='o')
        plt.title(title)
        plt.xlabel(x_label)
        plt.ylabel(y_label)  # 使用传入的y_label参数，而不是硬编码
        plt.xticks(rotation=45)
        plt.grid(True)
        
        # 设置Y轴格式，不使用科学计数法和千位分隔符
        from matplotlib.ticker import FuncFormatter
        plt.gca().yaxis.set_major_formatter(FuncFormatter(lambda x, p: f'{int(x)}'))
        
        # 保存到文件
        plt.savefig(file_path, format='png', bbox_inches='tight')
        plt.close()

        # 转换为base64
        with open(file_path, 'rb') as f:
            chart_base64 = f"data:image/png;base64,{base64.b64encode(f.read()).decode('utf-8')}"
        return {
            "trend_chart": chart_base64,
            "saved_path": file_path  
        }


def generate_multi_merchant_trend_chart(data_list: list, date_field: str, value_field: str, title: str, x_label: str, y_label: str, analysis_type: str, value_converter=lambda x: float(x), group_field: str = None) -> dict:
    """
    生成多商户对比趋势图
    
    Args:
        data_list: 数据列表
        date_field: 日期字段名
        value_field: 值字段名
        title: 图表标题
        x_label: X轴标签
        y_label: Y轴标签
        analysis_type: 分析类型
        value_converter: 值转换函数
        group_field: 分组字段名（如商户ID字段）
        
    Returns:
        包含图表信息的字典
    """
    plt.close()
    
    # 按商户分组数据
    merchant_groups = {}
    for item in data_list:
        merchant_id = item.get(group_field)
        if merchant_id:
            if merchant_id not in merchant_groups:
                merchant_groups[merchant_id] = []
            merchant_groups[merchant_id].append(item)
    
    # 如果没有商户数据，回退到单商户模式
    if not merchant_groups:
        logging.warning("[多商户趋势图] 未找到商户分组数据，回退到单商户模式")
        return TrendAnalyzer.generate_trend_chart(data_list, date_field, value_field, title, x_label, y_label, analysis_type, value_converter)
    
    # 确保保存目录存在
    save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'charts')
    os.makedirs(save_dir, exist_ok=True)
    
    # 生成唯一文件名
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    filename = f'multi_merchant_{analysis_type}_{timestamp}.png'
    file_path = os.path.join(save_dir, filename)
    
    # 绘制多商户对比趋势图
    plt.figure(figsize=(12, 8))
    
    # 设置中文字体（复用之前的字体设置逻辑）
    font_set_success = False
    
    # 方法1: 使用fontManager中已加载的字体
    try:
        font_names = [f.name for f in fontManager.ttflist]
        chinese_fonts = ["思源黑体", "Source Han Sans", "Noto Sans CJK", "Microsoft YaHei", "SimHei", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei"]
        
        for font_name in chinese_fonts:
            matched_fonts = [f for f in font_names if font_name in f]
            if matched_fonts:
                plt.rcParams['font.family'] = matched_fonts[0]
                font_set_success = True
                break
    except Exception as e:
        print(f"检查已加载字体失败: {str(e)}")
    
    # 方法2: 如果方法1失败，尝试直接使用字体文件路径设置字体
    if not font_set_success:
        possible_font_paths = [
            os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fonts', '思源黑体CN-Heavy.otf'),
            os.path.join(os.getcwd(), 'fonts', '思源黑体CN-Heavy.otf'),
            '/home/jiuyv/ccagent/fonts/思源黑体CN-Heavy.otf',
            '/home/jiuyy/ccagent/fonts/思源黑体CN-Heavy.otf',
            '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
            '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
            '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
            'C:/Windows/Fonts/simhei.ttf',
            'C:/Windows/Fonts/msyh.ttf',
            'C:/Windows/Fonts/msyh.ttc',
        ]
        
        font_path = None
        for path in possible_font_paths:
            if os.path.exists(path):
                font_path = path
                break
        
        if font_path:
            try:
                font_prop = FontProperties(fname=font_path)
                plt.rcParams['font.family'] = font_prop.get_name()
                font_set_success = True
            except Exception as e:
                print(f"加载字体失败: {str(e)}")
    
    # 方法3: 如果所有方法都失败，直接使用英文标签
    if not font_set_success:
        if "金额" in y_label:
            y_label = y_label.replace("金额", "Amount")
        if "日期" in x_label:
            x_label = x_label.replace("日期", "Date")
        if "趋势" in title:
            title = title.replace("趋势", "Trend")
        if "分析" in title:
            title = title.replace("分析", "Analysis")
        if "图表" in title:
            title = title.replace("图表", "Chart")
        if "统计" in title:
            title = title.replace("统计", "Statistics")
        if "报告" in title:
            title = title.replace("报告", "Report")
        
        plt.rcParams['font.family'] = 'DejaVu Sans'
        font_set_success = True
    
    plt.rcParams['axes.unicode_minus'] = False
    
    # 为每个商户生成趋势线
    colors = ['blue', 'red', 'green', 'orange', 'purple', 'brown', 'pink', 'gray', 'olive', 'cyan']
    marker_styles = ['o', 's', '^', 'D', 'v', '<', '>', 'p', '*', 'h']
    
    merchant_count = 0
    for i, (merchant_id, merchant_data) in enumerate(merchant_groups.items()):
        if merchant_count >= 10:  # 最多显示10个商户
            break
            
        # 提取日期和数值
        dates = [item.get(date_field) for item in merchant_data if item.get(date_field)]
        values = [value_converter(item.get(value_field, '0')) for item in merchant_data if item.get(value_field)]
        
        if dates and values:
            # 使用商户ID作为标签（如果可能，可以传入商户名称映射）
            label = f"商户{merchant_id[:8]}..." if len(merchant_id) > 10 else f"商户{merchant_id}"
            
            plt.plot(dates, values, 
                    marker=marker_styles[i % len(marker_styles)], 
                    color=colors[i % len(colors)], 
                    label=label, 
                    linewidth=2,
                    markersize=6)
            merchant_count += 1
    
    # 设置图表属性
    plt.title(f"{title} - 多商户对比")
    plt.xlabel(x_label)
    plt.ylabel(y_label)
    plt.xticks(rotation=45)
    plt.grid(True, alpha=0.3)
    plt.legend(loc='best', fontsize=8)
    
    # 设置Y轴格式
    from matplotlib.ticker import FuncFormatter
    plt.gca().yaxis.set_major_formatter(FuncFormatter(lambda x, p: f'{int(x)}'))
    
    # 保存到文件
    plt.savefig(file_path, format='png', bbox_inches='tight')
    plt.close()

    # 转换为base64
    with open(file_path, 'rb') as f:
        chart_base64 = f"data:image/png;base64,{base64.b64encode(f.read()).decode('utf-8')}"
    
    logging.info(f"[多商户趋势图] 生成成功，包含{merchant_count}个商户的对比趋势图")
    return {
        "trend_chart": chart_base64,
        "saved_path": file_path,
        "merchant_count": merchant_count
    }



# 获取用户关联的商户列表
async def get_user_merchants(phone_number: str, return_empty_on_error: bool = False) -> List[Dict[str, Any]]:
    """
    获取用户关联的商户列表
    
    Args:
        phone_number: 用户手机号
        return_empty_on_error: 是否在出错时返回空列表而不是抛出异常
        
    Returns:
        商户列表
    """
    start_time = time.time()
    logging.info(f"[步骤1-获取商户列表] 开始获取用户关联商户列表")
    
    # 验证手机号参数
    if not phone_number or not isinstance(phone_number, str) or not phone_number.strip():
        logging.error(f"[参数验证] 手机号参数无效: {phone_number}")
        if return_empty_on_error:
            logging.warning(f"[参数验证] 手机号参数无效，返回空列表")
            return []
        raise HTTPException(status_code=400, detail="手机号参数无效")
    
    try:
        # 创建一个专用的服务实例，使用更短的超时时间
        from base_service import BaseService
        short_timeout_service = BaseService()
        short_timeout_service.timeout = aiohttp.ClientTimeout(total=5)  # 5秒超时
        
        try:
            # 调用商户查询API（call_api已经返回解析后的字典）
            data = await short_timeout_service.call_api(Config.ETC_MERCHANT_API_PATH, phone_number, max_retries=0, base_url=Config.ETC_API_BASE_URL)
        finally:
            # 确保关闭服务实例的会话和连接器
            await short_timeout_service.close_session()
        
        # 类型检查：确保返回的是字典类型
        if not isinstance(data, dict):
            logging.error(f"[API调用] 返回类型错误，期望字典但得到: {type(data)}")
            if return_empty_on_error:
                logging.warning(f"[API调用] 返回类型错误，返回空列表")
                return []
            raise HTTPException(status_code=500, detail="API返回类型错误")
        
        # 检查API调用是否成功
        if data.get('code') != '10000':
            logging.error(f"[API错误] 获取商户列表失败: {data.get('msg')}")
            error_msg = data.get('msg', '未知错误')
            if return_empty_on_error:
                logging.warning(f"[API错误] 获取商户列表失败，返回空列表: {error_msg}")
                return []
            raise HTTPException(status_code=400, detail=f"获取商户列表失败：{error_msg}")
        
        merchants = data.get('respData', [])
        
        # 确保merchants是列表类型
        if not isinstance(merchants, list):
            logging.error(f"[数据处理] 商户数据格式错误，期望列表但得到: {type(merchants)}")
            merchants = []
        
        # 验证商户数据
        valid_merchants = []
        for merchant in merchants:
            if isinstance(merchant, dict) and merchant.get('merId') and merchant.get('merName'):
                valid_merchants.append(merchant)
            else:
                logging.warning(f"[数据处理] 跳过无效的商户数据: {merchant}")
        
        # 如果商户列表为空，不再获取清算单位并转换为商户格式
        # 清算单位和商户应该走不同的路由，而不是混合在一起
        if not valid_merchants:
            logging.info(f"[步骤1-获取商户列表] 未获取到商户列表，清算单位将通过普通路由处理")
        
        end_time = time.time()
        logging.info(f"[步骤1-获取商户列表] 获取商户列表耗时: {end_time - start_time:.2f}秒，共找到{len(valid_merchants)}个有效商户")
        return valid_merchants
    except HTTPException:
        # 直接抛出HTTP异常
        if return_empty_on_error:
            logging.warning(f"[HTTP异常] 获取商户列表失败，返回空列表")
            return []
        raise
    except Exception as e:
        logging.error(f"[获取商户列表] 错误: {str(e)}")
        if return_empty_on_error:
            logging.warning(f"[获取商户列表] 出现异常，返回空列表: {str(e)}")
            return []
        raise HTTPException(status_code=500, detail=f"获取商户列表失败：{str(e)}")

# 获取用户资源权限
async def get_user_resource_permissions(phone_number: str, mer_id: str) -> List[Dict[str, Any]]:
    """
    获取用户资源权限
    
    Args:
        phone_number: 用户手机号
        mer_id: 商户ID
        
    Returns:
        权限列表
    """
    start_time = time.time()
    logging.info(f"[步骤1-获取用户权限] 开始获取用户资源权限")
    
    # 验证参数
    if not phone_number or not isinstance(phone_number, str) or not phone_number.strip():
        logging.error(f"[参数验证] 手机号参数无效: {phone_number}")
        raise HTTPException(status_code=400, detail="手机号参数无效")
    
    if not mer_id or not isinstance(mer_id, str) or not mer_id.strip():
        logging.error(f"[参数验证] 商户ID参数无效: {mer_id}")
        raise HTTPException(status_code=400, detail="商户ID参数无效")
    
    params = {
        "userId": phone_number,
        "merId": mer_id
    }
    
    try:
        # 调用权限查询API（call_api已经返回解析后的字典）
        # 根据curl命令格式，使用JSON参数格式和正确的URL
        data = await service.call_api(Config.ETC_RESOURCE_API_PATH, params, base_url=Config.ETC_API_BASE_URL)
        
        # 类型检查：确保返回的是字典类型
        if not isinstance(data, dict):
            logging.error(f"[API调用] 返回类型错误，期望字典但得到: {type(data)}")
            raise HTTPException(status_code=500, detail="API返回类型错误")
        
        # 根据实际返回格式，使用"res"字段而不是"code"字段检查状态
        if data.get('res') != '10000':
            logging.error(f"[API错误] 获取用户权限失败: {data.get('msg')}")
            error_msg = data.get('msg', '未知错误')
            raise HTTPException(status_code=400, detail=f"获取用户权限失败：{error_msg}")
        
        permissions = data.get('respData', [])
        
        # 确保permissions是列表类型
        if not isinstance(permissions, list):
            logging.error(f"[数据处理] 权限数据格式错误，期望列表但得到: {type(permissions)}")
            permissions = []
        
        # 验证权限数据
        valid_permissions = []
        for permission in permissions:
            if isinstance(permission, dict):
                valid_permissions.append(permission)
            else:
                logging.warning(f"[数据处理] 跳过无效的权限数据: {permission}")
        
        end_time = time.time()
        logging.info(f"[步骤1-获取用户权限] 获取用户权限耗时: {end_time - start_time:.2f}秒，共找到{len(valid_permissions)}个有效权限")
        return valid_permissions
    except HTTPException:
        # 直接抛出HTTP异常
        raise
    except Exception as e:
        logging.error(f"[获取用户权限] 错误: {str(e)}")
        raise HTTPException(status_code=500, detail=f"获取用户权限失败：{str(e)}")

# 查询厂库交易数据汇总
async def get_etc_transaction_summary(start_date: str, end_date: str, sub_station_ids: Optional[List[str]] = None, phone_number: Optional[str] = None) -> Dict[str, Any]:
    """
    查询厂库交易数据汇总
    
    Args:
        start_date: 开始日期
        end_date: 结束日期
        sub_station_ids: 子站点ID列表（可选），如果不提供则使用用户所有商户ID
        phone_number: 用户手机号（可选），用于获取用户商户列表
        
    Returns:
        交易汇总数据
    """
    start_time = time.time()
    logging.info(f"[步骤1-获取交易汇总] 开始获取ETC交易数据汇总")
    
    # 验证日期参数
    if not start_date or not isinstance(start_date, str) or not start_date.strip():
        logging.error(f"[参数验证] 开始日期参数无效: {start_date}")
        raise HTTPException(status_code=400, detail="开始日期参数无效")
    
    if not end_date or not isinstance(end_date, str) or not end_date.strip():
        logging.error(f"[参数验证] 结束日期参数无效: {end_date}")
        raise HTTPException(status_code=400, detail="结束日期参数无效")
    
    params = {
        "startStlmDate": start_date,
        "endStlmDate": end_date
    }
    
    # 如果没有提供sub_station_ids但有手机号，则获取用户所有商户ID作为sub_station_ids
    if not sub_station_ids and phone_number:
        try:
            merchants = await get_user_merchants(phone_number, return_empty_on_error=True)
            if merchants:
                sub_station_ids = [merchant['merId'] for merchant in merchants if merchant.get('merId')]
                logging.info(f"[步骤1-获取交易汇总] 使用用户所有商户ID作为subStationIds: {sub_station_ids}")
        except Exception as e:
            logging.warning(f"[步骤1-获取交易汇总] 获取用户商户列表失败: {str(e)}")
    
    if sub_station_ids:
        # 验证sub_station_ids参数
        if isinstance(sub_station_ids, list):
            # 过滤掉空值并确保都是字符串
            valid_ids = [str(id).strip() for id in sub_station_ids if id and str(id).strip()]
            if valid_ids:
                params["subStationIds"] = valid_ids
            else:
                logging.info(f"[参数验证] sub_station_ids列表中没有有效的ID，将不传递subStationIds参数")
        elif isinstance(sub_station_ids, str) and sub_station_ids.strip():
            # 如果是字符串，按逗号分割并转换为列表
            valid_ids = [id.strip() for id in sub_station_ids.split(',') if id.strip()]
            if valid_ids:
                params["subStationIds"] = valid_ids
            else:
                logging.info(f"[参数验证] sub_station_ids字符串中没有有效的ID，将不传递subStationIds参数")
        else:
            logging.info(f"[参数验证] sub_station_ids参数格式无效: {sub_station_ids}，将不传递subStationIds参数")
    else:
        logging.info(f"[参数验证] 未提供sub_station_ids参数，将查询所有子站点数据")
    
    try:
        # 调用交易汇总API（call_api已经返回解析后的字典）
        logging.info(f"[API调用] 请求URL: {Config.ETC_API_BASE_URL}{Config.ETC_SETT_API_PATH}")
        logging.info(f"[API调用] 请求参数: {params}")
        
        data = await service.call_api(Config.ETC_SETT_API_PATH, params, base_url=Config.ETC_API_BASE_URL)
        
        # 记录原始响应数据
        logging.info(f"[API调用] 原始响应数据: {data}")
        
        # 类型检查：确保返回的是字典类型
        if not isinstance(data, dict):
            logging.error(f"[API调用] 返回类型错误，期望字典但得到: {type(data)}")
            raise HTTPException(status_code=500, detail="API返回类型错误")
        
        # 检查API调用是否成功
        # 特殊处理：API可能返回respCode而不是code，且respCode=100（数字或字符串）也表示成功
        code_success = data.get('code') == '10000'
        resp_code_success = data.get('respCode') in [100, '100']  # 同时支持数字和字符串
        
        if not code_success and not resp_code_success:
            error_msg = data.get('msg', data.get('respMsg', '未知错误'))
            logging.error(f"[API错误] 获取交易汇总失败: {error_msg}")
            logging.error(f"[API错误] 完整响应数据: {data}")
            raise HTTPException(status_code=400, detail=f"获取交易汇总失败：{error_msg}")
        
        # 检查是否有响应数据
        if not data.get('respData') and not data.get('resultDOS'):
            logging.warning(f"[API警告] 响应成功但没有数据，code={data.get('code')}, respCode={data.get('respCode')}")
        
        # 从respData或resultDOS获取交易数据（根据日志显示，数据在resultDOS字段中）
        transactions = data.get('respData', data.get('resultDOS', []))
        
        # 确保transactions是列表类型
        if not isinstance(transactions, list):
            logging.error(f"[数据处理] 交易数据格式错误，期望列表但得到: {type(transactions)}")
            transactions = []
        
        # 验证交易数据
        valid_transactions = []
        for transaction in transactions:
            if isinstance(transaction, dict):
                valid_transactions.append(transaction)
            else:
                logging.warning(f"[数据处理] 跳过无效的交易数据: {transaction}")
        
        # 更新响应数据中的交易列表
        if valid_transactions != transactions:
            data['respData'] = valid_transactions
            logging.info(f"[数据处理] 过滤后的有效交易记录数: {len(valid_transactions)}")
        
        end_time = time.time()
        logging.info(f"[步骤1-获取交易汇总] 获取交易汇总耗时: {end_time - start_time:.2f}秒，共找到{len(valid_transactions)}条有效记录")
        return data
    except HTTPException:
        # 直接抛出HTTP异常
        raise
    except Exception as e:
        logging.error(f"[获取交易汇总] 错误: {str(e)}")
        logging.error(f"[获取交易汇总] 错误类型: {type(e).__name__}")
        raise HTTPException(status_code=500, detail=f"获取交易汇总失败：{str(e)}")

# 相似度计算函数
def calculate_similarity(str1: str, str2: str) -> float:
    """
    计算两个字符串的相似度（0-1之间）
    
    Args:
        str1: 第一个字符串
        str2: 第二个字符串
        
    Returns:
        相似度分数，0表示完全不相似，1表示完全相同
    """
    # 使用SequenceMatcher计算相似度
    similarity = SequenceMatcher(None, str1.lower(), str2.lower()).ratio()
    return similarity

# 公共函数：处理ETC服务调用的公共逻辑
async def call_etc_service_common(
    user_id: str,
    user_query: str,
    params: Dict[str, Any],
    process_func: callable,
    conversation_id: Optional[str] = None,
    include_analysis: Optional[bool] = None,
    include_chart: Optional[bool] = None,
    service_name: str = "ETC服务"
) -> dict:
    """
    处理ETC服务调用的公共逻辑
    
    Args:
        user_id: 用户ID
        user_query: 用户查询
        params: 参数字典
        process_func: 数据处理函数
        conversation_id: 对话ID
        include_analysis: 是否包含分析
        include_chart: 是否包含图表
        service_name: 服务名称（用于日志）
        
    Returns:
        服务调用结果
    """
    start_time = time.time()
    
    # 检查是否是监控模式
    is_monitor = params.get("is_monitor", False)
    
    # 如果是监控请求，添加特殊标识到查询中，确保每次都是真实调用
    if is_monitor:
        monitor_tag = f"[MONITOR_{int(time.time())}]"
        user_query = f"{monitor_tag} {user_query}"
        logging.info(f"[步骤2-{service_name}调用] 监控模式调用{service_name}，已添加监控标识")
    
    logging.info(f"[步骤2-{service_name}调用] 开始调用{service_name}")
    
    # 如果传入了conversation_id，则使用它；否则创建一个新的本地对话ID
    local_conversation_id = conversation_id if conversation_id else chat_history_manager.create_new_conversation()
    logging.info(f"[对话管理] 使用对话ID: {local_conversation_id}")
    
    # 获取对话历史
    history = chat_history_manager.get_history(local_conversation_id)
    logging.info(f"[对话管理] 历史消息数量: {len(history)}")
    if history:
        logging.info(f"[对话管理] 最近一条历史消息: {history[-1]['content'][:50]}...")
    
    # 处理数据，直接传递可选参数，保持None值以便在服务层进行自动判断
    # 确保params中包含user_id，以便在处理商户名称时使用
    if 'userId' not in params and 'phoneNo' not in params:
        params['userId'] = user_id
        params['phoneNo'] = user_id
    
    result = await process_func(
        user_query=user_query,
        phone_number=user_id,
        conversation_id=local_conversation_id,
        include_analysis=include_analysis,
        include_chart=include_chart,
        timeout=params.get("timeout", 60),
        params=params
    )
    
    # 保存对话记录到本地对话ID中
    chat_history_manager.add_message(local_conversation_id, "user", user_query)
    chat_history_manager.add_message(local_conversation_id, "assistant", result.get("summary", ""))
    
    end_time = time.time()
    logging.info(f"[步骤2-{service_name}调用] 调用{service_name}耗时: {end_time - start_time:.2f}秒")
    return {
        "answer": result.get("summary", ""),
        "conversation_id": local_conversation_id,
        "result": result
    }

# 公共函数：处理数据分析和趋势图生成
async def process_analysis_and_chart(
    data: Dict[str, Any],
    data_list: List[Any],
    user_query: str,
    analysis_type: str,
    include_analysis: Optional[bool] = None,
    include_chart: Optional[bool] = None,
    date_field: Optional[str] = None,
    value_field: Optional[str] = None,
    title: Optional[str] = None,
    x_label: Optional[str] = None,
    y_label: Optional[str] = None,
    value_converter: Optional[callable] = None,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None,
    filter_field: Optional[str] = None,
    filter_value: Optional[str] = None,
    group_field: Optional[str] = None
) -> Dict[str, Any]:
    """
    处理数据分析和趋势图生成的公共函数
    
    Args:
        data: 原始数据字典
        data_list: 用于趋势图的数据列表
        user_query: 用户查询
        analysis_type: 分析类型
        include_analysis: 是否包含分析
        include_chart: 是否包含图表
        date_field: 日期字段名
        value_field: 值字段名
        title: 图表标题
        x_label: X轴标签
        y_label: Y轴标签
        value_converter: 值转换函数
        start_date: 开始日期（用于分析）
        end_date: 结束日期（用于分析）
        filter_field: 过滤字段名（可选）
        filter_value: 过滤字段值（可选）
        group_field: 分组字段名（用于多商户对比，可选）
        
    Returns:
        包含分析和图表结果的字典
    """
    # 智能判断是否需要数据分析和趋势图
    do_analysis = include_analysis if include_analysis is not None else should_include_analysis(user_query, analysis_type)
    do_chart = include_chart if include_chart is not None else should_include_chart(user_query, analysis_type)
    
    # 准备结果变量
    trend_data = None
    text_analysis = None
    
    # 根据判断结果决定是否执行数据分析和趋势图生成
    if do_analysis or do_chart:
        tasks = []
        
        # 如果需要过滤数据，先进行过滤
        filtered_data_list = data_list
        if filter_field and filter_value and data_list:
            filtered_data_list = [
                item for item in data_list 
                if item.get(filter_field) == filter_value
            ]
            logging.info(f"[数据分析] 根据字段'{filter_field}'='{filter_value}'过滤数据，原始数据{len(data_list)}条，过滤后{len(filtered_data_list)}条")
        
        if do_chart and filtered_data_list and date_field and value_field:
            # 设置默认值
            chart_title = title or f"{analysis_type}趋势分析"
            chart_x_label = x_label or "日期"
            chart_y_label = y_label or "值"
            chart_value_converter = value_converter or (lambda x: float(x) if x else 0)
            
            # 创建趋势图生成任务
            trend_task = asyncio.create_task(
                asyncio.to_thread(
                    TrendAnalyzer.generate_trend_chart,
                    filtered_data_list,
                    date_field,
                    value_field,
                    chart_title,
                    chart_x_label,
                    chart_y_label,
                    analysis_type,
                    chart_value_converter,
                    group_field
                )
            )
            tasks.append(('chart', trend_task))
        
        if do_analysis:
            # 创建文本数据分析任务，确保传递字典类型
            if isinstance(data, dict):
                analysis_params = {"data": data, "analysis_type": analysis_type}
                if start_date and end_date:
                    analysis_params["start_date"] = start_date
                    analysis_params["end_date"] = end_date
                    
                analysis_task = asyncio.create_task(
                    analyze_data(**analysis_params)
                )
                tasks.append(('analysis', analysis_task))
            else:
                logging.error(f"[数据分析] 无法对非字典类型进行分析: {type(data)}")
        
        # 等待所有任务完成
        if tasks:
            done, pending = await asyncio.wait([task for _, task in tasks], return_when=asyncio.ALL_COMPLETED)
            
            for task_type, task in tasks:
                if task in done:
                    try:
                        result = task.result()
                        if task_type == 'chart':
                            trend_data = result
                            logging.info(f"[趋势分析] 生成{analysis_type}趋势图成功")
                        elif task_type == 'analysis':
                            text_analysis = result
                    except Exception as e:
                        if task_type == 'chart':
                            logging.error(f"[趋势分析] 生成趋势图失败: {str(e)}")
                        elif task_type == 'analysis':
                            logging.error(f"[数据分析] 分析失败: {str(e)}")
    
    return {
        "trend_data": trend_data,
        "text_analysis": text_analysis
    }

# 公共函数：生成统一的单位权限错误信息
def generate_unit_permission_error(unit_list: list, custom_message: Optional[str] = None) -> str:
    """
    生成统一的单位权限错误信息
    修改后不再返回404错误，而是返回提示信息
    
    Args:
        unit_list: 单位列表
        custom_message: 自定义错误信息
        
    Returns:
        str: 统一格式的错误信息
    """
    if custom_message:
        return custom_message
        
    available_units = [safe_extract_unit_name(unit) for unit in unit_list]
    
    if not available_units or all(unit == "未知" for unit in available_units):
        return "您的微信手机号未绑定到任何清算单位或商户，请联系您的客户经理，完成绑定后可使用本清算智能体的功能"
    else:
        return f"您没有权限查询此单位，请去统一对账平台进行绑定。您可以查询的单位有：{', '.join(available_units)}"

# 公共函数：安全提取商户名称
def safe_extract_merchant_name(merchant: Any, default: str = "未知") -> str:
    """
    安全地从商户对象中提取商户名称
    
    Args:
        merchant: 商户对象（可能是字典、None或其他类型）
        default: 默认返回值
        
    Returns:
        str: 商户名称或默认值
    """
    if not isinstance(merchant, dict):
        return default
        
    merchant_name = merchant.get("merName")
    if merchant_name and isinstance(merchant_name, str) and merchant_name.strip():
        return merchant_name.strip()
    else:
        return default

# 公共函数：安全提取单位名称
def safe_extract_unit_name(unit: Any, default: str = "未知") -> str:
    """
    安全地从单位对象中提取单位名称
    
    Args:
        unit: 单位对象（可能是字典、None或其他类型）
        default: 默认返回值
        
    Returns:
        str: 单位名称或默认值
    """
    if not isinstance(unit, dict):
        return default
        
    unit_name = unit.get("unitFullName")
    if unit_name and isinstance(unit_name, str) and unit_name.strip():
        return unit_name.strip()
    else:
        return default

# 公共函数：安全提取单位ID
def safe_extract_unit_id(unit: Any, default: str = "未知") -> str:
    """
    安全地从单位对象中提取单位ID
    
    Args:
        unit: 单位对象（可能是字典、None或其他类型）
        default: 默认返回值
        
    Returns:
        str: 单位ID或默认值
    """
    if not isinstance(unit, dict):
        return default
        
    unit_id = unit.get("unitId")
    if unit_id and isinstance(unit_id, str) and unit_id.strip():
        return unit_id.strip()
    else:
        return default

# 公共函数：验证手机号格式
def validate_phone_number(phone_number: str) -> bool:
    """
    验证手机号格式是否正确
    
    Args:
        phone_number: 手机号
        
    Returns:
        bool: 格式是否正确
        
    Raises:
        HTTPException: 如果手机号格式不正确
    """
    phone_pattern = re.compile(r'^1[3-9]\d{9}$')
    if not phone_pattern.match(phone_number):
        raise HTTPException(status_code=400, detail="手机号格式不正确（需为11位有效手机号）")
    return True

# 公共函数：处理文本，移除常见后缀
def process_text_suffixes(text: str) -> str:
    """
    处理文本，移除常见单位后缀
    
    Args:
        text: 原始文本（可以是关键词或单位名称）
        
    Returns:
        str: 处理后的文本
    """
    suffixes = ['公司', '有限公司', '分公司', '集团', '子公司']
    processed_text = text.lower().strip()
    for suffix in suffixes:
        if processed_text.endswith(suffix):
            processed_text = processed_text[:-len(suffix)]
            break
    return processed_text

# 公共函数：处理停车场名称，提取核心地理位置信息
def process_parking_name(text: str) -> str:
    """
    处理停车场名称，提取核心地理位置信息
    
    Args:
        text: 原始文本（停车场名称）
        
    Returns:
        str: 处理后的核心地理位置信息
    """
    processed_text = text.lower().strip()
    
    # 移除常见的停车场后缀（包含大小写变体）
    parking_suffixes = ['p+r', '停车场', '停车库', 'p&r', 'parking', 'park and ride']
    for suffix in parking_suffixes:
        if processed_text.endswith(suffix):
            processed_text = processed_text[:-len(suffix)]
            break
        elif suffix in processed_text:
            # 如果后缀在中间，也移除
            parts = processed_text.split(suffix)
            if len(parts) >= 2:
                processed_text = parts[0].strip()
                break
    
    # 移除常见的连接词和修饰词（但不包括"路"，因为很多地名包含"路"）
    connectors = ['的', '之', '中', '中心', '广场', '商场', '大厦', '大楼', '园区', '基地', '站', '街', '道']
    for connector in connectors:
        if processed_text.endswith(connector):
            processed_text = processed_text[:-len(connector)]
    
    # 移除括号内容
    import re
    processed_text = re.sub(r'\(.*?\)|\[.*?\]|【.*?】', '', processed_text)
    
    # 去除多余空格
    processed_text = re.sub(r'\s+', '', processed_text)
    
    # 对于特别短的名称（如"共和新"），不做进一步处理
    if len(processed_text) <= 3:
        return processed_text
    
    return processed_text

# 公共函数：检查是否为停车场查询
def is_parking_query(text: str) -> bool:
    """
    检查文本是否为停车场相关查询
    
    Args:
        text: 查询文本
        
    Returns:
        bool: 是否为停车场查询
    """
    text_lower = text.lower()
    parking_indicators = ['p+r', '停车场', '停车库', 'p&r', 'parking', 'park and ride']
    
    # 检查是否包含停车场相关关键词（包含大小写变体）
    for indicator in parking_indicators:
        if indicator in text_lower:
            return True
    
    return False

# 公共函数：检查是否为公司/机构名称
def is_company_name(text: str) -> bool:
    """
    检查文本是否为公司/机构名称
    
    Args:
        text: 文本
        
    Returns:
        bool: 是否为公司/机构名称
    """
    text_lower = text.lower()
    company_indicators = ['公司', '有限公司', '分公司', '集团', '子公司', '企业', '机构', '单位', '部门']
    
    # 检查是否包含公司相关关键词
    for indicator in company_indicators:
        if indicator in text_lower:
            return True
    
    return False

# 公共函数：计算地理位置相似度
def calculate_location_similarity(keyword: str, unit_name: str) -> float:
    """
    计算地理位置名称的相似度，简化版本不区分停车场和公司
    
    Args:
        keyword: 用户查询的关键词
        unit_name: 单位名称
        
    Returns:
        float: 相似度得分（0-1之间）
    """
    from difflib import SequenceMatcher
    
    # 计算完整名称的相似度
    full_similarity = SequenceMatcher(None, keyword.lower(), unit_name.lower()).ratio()
    
    # 处理关键词和单位名称，移除常见后缀
    # 先尝试使用停车场处理函数，如果包含停车场后缀
    if is_parking_query(keyword) or is_parking_query(unit_name):
        processed_keyword = process_parking_name(keyword.lower())
        processed_unit_name = process_parking_name(unit_name.lower())
    else:
        processed_keyword = process_text_suffixes(keyword.lower())
        processed_unit_name = process_text_suffixes(unit_name.lower())
    
    # 计算处理后的相似度
    processed_similarity = SequenceMatcher(None, processed_keyword, processed_unit_name).ratio()
    
    # 子串匹配得分
    substring_score = 0.0
    if processed_keyword in processed_unit_name:
        substring_score = 0.7  # 降低子串匹配得分，避免"P+R"这样的通用词影响过大
    elif processed_unit_name in processed_keyword:
        substring_score = 0.6  # 降低子串匹配得分
    elif any(word in processed_unit_name for word in processed_keyword.split() if len(word) > 1):
        substring_score = 0.4  # 降低部分词匹配得分
    
    # 对于包含"P+R"的查询，额外降低相似度权重，避免匹配到其他P+R停车场
    if 'p+r' in keyword.lower() or 'p&r' in keyword.lower():
        # 只有当核心地理位置部分匹配时才给予较高分数
        core_keyword = process_parking_name(keyword.lower())
        core_unit_name = process_parking_name(unit_name.lower())
        core_similarity = SequenceMatcher(None, core_keyword, core_unit_name).ratio()
        
        # 如果核心地理位置相似度不高，则大幅降低总相似度
        if core_similarity < 0.7:
            return max(full_similarity, processed_similarity, substring_score) * 0.5
    
    # 返回最高相似度得分
    return max(full_similarity, processed_similarity, substring_score)

# 公共函数：LLM智能匹配
async def llm_smart_match(keyword: str, unit_list: list, return_multiple: bool = False, history_context: str = "") -> list:
    """
    使用LLM进行智能匹配
    
    Args:
        keyword: 查询关键词
        unit_list: 单位列表
        return_multiple: 是否返回多个匹配结果
        history_context: 历史对话上下文
        
    Returns:
        list: 匹配的单位ID列表
    """
    # 如果单位列表为空，直接返回空列表
    if not unit_list:
        return []
    
    # 构建单位列表信息，使用安全提取函数
    units_info_list = []
    similarity_scores = []
    
    # 计算每个单位与关键词的相似度
    for i, unit in enumerate(unit_list):
        unit_name = safe_extract_unit_name(unit)
        unit_id = safe_extract_unit_id(unit)
        
        # 使用新的地理位置相似度计算方法
        location_similarity = calculate_location_similarity(keyword, unit_name)
        
        # 1. 整体名称相似度
        from difflib import SequenceMatcher
        overall_similarity = SequenceMatcher(None, keyword.lower(), unit_name.lower()).ratio()
        
        # 2. 关键词匹配：检查关键词是否是单位名称的子串或包含单位名称的主要部分
        keyword_lower = keyword.lower()
        unit_name_lower = unit_name.lower()
        
        # 先尝试使用停车场处理函数，如果包含停车场后缀
        if is_parking_query(keyword) or is_parking_query(unit_name):
            processed_keyword = process_parking_name(keyword_lower)
            processed_unit_name = process_parking_name(unit_name_lower)
        else:
            processed_keyword = process_text_suffixes(keyword_lower)
            processed_unit_name = process_text_suffixes(unit_name_lower)
        
        # 3. 处理后的名称相似度
        processed_similarity = SequenceMatcher(None, processed_keyword, processed_unit_name).ratio()
        
        # 4. 子串匹配得分
        substring_score = 0.0
        if processed_keyword in processed_unit_name:
            substring_score = 0.7  # 降低子串匹配得分，避免"P+R"这样的通用词影响过大
        elif processed_unit_name in processed_keyword:
            substring_score = 0.6  # 降低子串匹配得分
        elif any(word in processed_unit_name for word in processed_keyword.split() if len(word) > 1):
            substring_score = 0.4  # 降低部分词匹配得分
        
        # 对于包含"P+R"的查询，额外降低相似度权重，避免匹配到其他P+R停车场
        if 'p+r' in keyword.lower() or 'p&r' in keyword.lower():
            # 只有当核心地理位置部分匹配时才给予较高分数
            core_keyword = process_parking_name(keyword.lower())
            core_unit_name = process_parking_name(unit_name.lower())
            core_similarity = SequenceMatcher(None, core_keyword, core_unit_name).ratio()
            
            # 如果核心地理位置相似度不高，则大幅降低总相似度
            if core_similarity < 0.7:
                location_similarity *= 0.5
                overall_similarity *= 0.5
                processed_similarity *= 0.5
                substring_score *= 0.5
        
        # 5. 综合相似度计算
        # 使用综合相似度计算，不区分停车场和公司
        final_similarity = max(location_similarity, overall_similarity, processed_similarity, substring_score)
        
        similarity_scores.append({
            "index": i,
            "unit_id": unit_id,
            "unit_name": unit_name,
            "similarity": final_similarity,
            "location_similarity": location_similarity
        })
    
    # 按相似度降序排序
    similarity_scores.sort(key=lambda x: x["similarity"], reverse=True)
    
    # 限制单位列表显示，避免提示词过长
    # 根据单位总数动态调整显示数量，提高匹配准确率
    total_units = len(similarity_scores)
    if total_units <= 10:
        # 如果单位总数不超过10个，显示所有单位
        max_units = total_units
    elif total_units <= 50:
        # 如果单位总数在10-50之间，显示前15个
        max_units = 15
    else:
        # 如果单位总数超过50个，显示前20个
        max_units = 20
    
    if total_units > max_units:
        # 如果单位数量超过限制，只显示相似度最高的前max_units个
        display_units = similarity_scores[:max_units]
        for unit in display_units:
            units_info_list.append(f"{unit['index']+1}. {unit['unit_name']} (ID: {unit['unit_id']}, 相似度: {unit['similarity']:.2f})")
        units_info = "\n".join(units_info_list)
        units_info += f"\n...（还有{total_units - max_units}个单位未显示，已按相似度排序）"
    else:
        # 如果单位数量不多，显示所有单位，并按相似度排序
        for unit in similarity_scores:
            units_info_list.append(f"{unit['index']+1}. {unit['unit_name']} (ID: {unit['unit_id']}, 相似度: {unit['similarity']:.2f})")
        units_info = "\n".join(units_info_list)
    
    # 根据是否需要多个结果构建不同的提示词
    # 对于停车场查询，添加特殊说明
    parking_note = ""
    if is_parking_query(keyword):
        parking_note = """
特别说明：用户查询的是停车场，请优先匹配名称中包含"P+R"、"P&R"或"停车场"的单位。
如果同时有公司和停车场都包含关键词，请优先选择停车场。
"""
    
    # 构建历史对话上下文部分
    history_section = ""
    if history_context:
        history_section = f"\n用户历史对话上下文：{history_context}\n"
    
    if return_multiple:
        match_prompt = f"""用户查询的关键词是：{keyword}
{history_section}
以下是用户可查询的单位列表（按与关键词的相似度从高到低排序，最多显示前{max_units}个）：
{units_info}
{parking_note}
请根据关键词和历史对话上下文，选择最可能匹配的单位。如果有多个可能匹配的单位，请按匹配度从高到低排序，只返回单位ID列表，用逗号分隔。
如果没有明显匹配的单位，请返回空字符串。

注意：
1. 单位ID是数字形式.
2. 请只返回单位ID，不要返回其他任何解释或说明。
3. 优先选择在历史对话中提到过的单位。

请返回单位ID列表："""
    else:
        match_prompt = f"""用户查询的关键词是：{keyword}
{history_section}
以下是用户可查询的单位列表（按与关键词的相似度从高到低排序，最多显示前{max_units}个）：
{units_info}
{parking_note}
请根据关键词和历史对话上下文，选择最可能匹配的单位。如果有多个可能匹配的单位，请选择最相似的一个，只返回一个单位ID。
如果没有明显匹配的单位，请返回空字符串。

注意：
1. 单位ID是数字形式，例如：12345
2. 请只返回单位ID，不要返回其他任何解释或说明。
3. 优先选择在历史对话中提到过的单位。

请返回单位ID："""
    
    try:
        # 如果提示词仍然过长（超过4000字符），使用关键词匹配作为备选方案
        if len(match_prompt) > 4000:
            logging.warning(f"[单位匹配] 提示词过长({len(match_prompt)}字符)，使用关键词匹配作为备选方案")
            return await _keyword_fallback_match(keyword, unit_list, return_multiple)
        
        logging.info(f"[单位匹配] 开始LLM智能匹配，提示词长度: {len(match_prompt)}")
        result = (await LLMService.call_qwen_model(match_prompt, temperature=0.0, timeout=30)).strip()
        logging.info(f"[单位匹配] LLM返回结果: {result}")
        
        if result:
            # 解析返回的单位ID
            # 尝试提取单位ID，可能包含在返回文本的任何位置
            import re
            
            # 尝试匹配单位ID模式（单位ID是数字）
            id_pattern = r'\b\d+\b'
            potential_ids = re.findall(id_pattern, result)
            
            # 如果没有找到匹配的模式，尝试按逗号分割
            if not potential_ids:
                if return_multiple:
                    potential_ids = [id.strip() for id in result.split(',')]
                else:
                    potential_ids = [result]
            
            logging.info(f"[单位匹配] 解析出的潜在单位ID: {potential_ids}")
            
            # 验证ID是否在单位列表中
            matched_ids = []
            for unit_id in potential_ids:
                if unit_id and isinstance(unit_id, str) and unit_id.strip():
                    # 检查是否在单位列表中存在
                    for unit in unit_list:
                        if isinstance(unit, dict) and unit.get("unitId") == unit_id.strip():
                            matched_ids.append(unit_id.strip())
                            logging.info(f"[单位匹配] LLM智能匹配成功: {unit_id} (单位名称: {safe_extract_unit_name(unit)})")
                            break
            
            if matched_ids:
                return matched_ids
            else:
                logging.info(f"[单位匹配] LLM返回的单位ID不在单位列表中，匹配失败")
        else:
            logging.info(f"[单位匹配] LLM返回空结果，匹配失败")
    except Exception as e:
        logging.error(f"智能匹配失败: {str(e)}")
        # 如果LLM匹配失败，使用关键词匹配作为备选方案
        return await _keyword_fallback_match(keyword, unit_list, return_multiple)
    
    logging.info(f"[单位匹配] LLM智能匹配未找到合适结果，返回空列表")
    return []

# 关键词匹配备选方案
async def _keyword_fallback_match(keyword: str, unit_list: list, return_multiple: bool = False) -> list:
    """
    当LLM匹配失败或提示词过长时，使用关键词匹配作为备选方案
    
    Args:
        keyword: 查询关键词
        unit_list: 单位列表
        return_multiple: 是否返回多个匹配结果
        
    Returns:
        list: 匹配的单位ID列表
    """
    try:
        # 计算每个单位名称与关键词的相似度
        similarity_scores = []
        for unit in unit_list:
            if isinstance(unit, dict):
                unit_name = safe_extract_unit_name(unit)
                unit_id = safe_extract_unit_id(unit)
                
                # 使用新的地理位置相似度计算方法（内部已包含停车场处理逻辑）
                similarity = calculate_location_similarity(keyword.lower().strip(), unit_name.lower().strip())
                similarity_scores.append((unit_id, unit_name, similarity))
        
        # 按相似度降序排序
        similarity_scores.sort(key=lambda x: x[2], reverse=True)
        
        # 获取匹配结果
        matched_ids = []
        if similarity_scores:
            # 如果只需要一个结果，返回相似度最高的
            if not return_multiple:
                best_match = similarity_scores[0]
                # 设置相似度阈值，只有超过阈值才认为是匹配
                if best_match[2] > 0.3:  # 相似度阈值设为0.3
                    matched_ids.append(best_match[0])
                    logging.info(f"[单位匹配] 关键词匹配成功: {best_match[0]} (相似度: {best_match[2]:.2f})")
            else:
                # 如果需要多个结果，返回所有相似度超过阈值的
                threshold = 0.3  # 相似度阈值
                for unit_id, unit_name, similarity in similarity_scores:
                    if similarity > threshold:
                        matched_ids.append(unit_id)
                        logging.info(f"[单位匹配] 关键词匹配成功: {unit_id} (相似度: {similarity:.2f})")
                    else:
                        # 由于已按相似度排序，一旦低于阈值就可以停止
                        break
        
        return matched_ids
    except Exception as e:
        logging.error(f"关键词匹配失败: {str(e)}")
        return []

# 获取最佳匹配单位函数（向后兼容接口）
async def get_best_match_unit(phone_number: str, keyword: str, similarity_threshold: float = 0.3) -> str:
    """
    根据手机号和关键词获取最佳匹配的单位ID
    注意：此函数保留作为向后兼容接口，内部调用新的get_unit_id_by_phone_and_keyword函数
    
    Args:
        phone_number: 用户手机号
        keyword: 查询关键词
        similarity_threshold: 相似度阈值（保留参数，但新逻辑中不再使用）
        
    Returns:
        最佳匹配的单位ID
        
    Raises:
        HTTPException: 如果没有找到匹配的单位
    """
    # 调用新的匹配函数
    matched_ids = await get_unit_id_by_phone_and_keyword(phone_number, keyword)
    
    # 返回第一个匹配的单位ID
    if matched_ids:
        return matched_ids[0]
    
    # 如果没有匹配到单位，抛出异常
    raise HTTPException(status_code=404, detail="未找到匹配的单位")
